Przeglądaj źródła

mac-capture: Add macOS Audio Capture

jcm 2 lat temu
rodzic
commit
239e9273dc

+ 3 - 2
plugins/mac-capture/CMakeLists.txt

@@ -11,8 +11,9 @@ target_sources(
           CGDisplayStream.h
           mac-audio.c
           mac-display-capture.m
-		  mac-sck-common.h
-		  mac-sck-common.m
+          mac-sck-audio-capture.m
+          mac-sck-common.h
+          mac-sck-common.m
           mac-sck-video-capture.m
           mac-window-capture.m
           plugin-main.c

+ 3 - 0
plugins/mac-capture/data/locale/en-US.ini

@@ -3,6 +3,8 @@ CoreAudio.OutputCapture="Audio Output Capture"
 CoreAudio.Device="Device"
 CoreAudio.Device.Default="Default"
 ApplicationCapture="Application Capture"
+ApplicationAudioCapture="Application Audio Capture"
+DesktopAudioCapture="Desktop Audio Capture"
 DisplayCapture="Display Capture"
 DisplayCapture.Display="Display"
 DisplayCapture.ShowCursor="Show cursor"
@@ -24,6 +26,7 @@ Crop.size.width="Crop right"
 Crop.size.height="Crop bottom"
 SCK.Name="macOS Screen Capture"
 SCK.Name.Beta="macOS Screen Capture (BETA)"
+SCK.Audio.Name="macOS Audio Capture"
 SCK.AudioUnavailable="Audio capture requires macOS 13 or newer."
 SCK.CaptureTypeUnavailable="Selected capture type requires macOS 13 or newer."
 SCK.Method="Method"

+ 323 - 0
plugins/mac-capture/mac-sck-audio-capture.m

@@ -0,0 +1,323 @@
+#include "mac-sck-common.h"
+
+const char *sck_audio_capture_getname(void *unused __unused)
+{
+    return obs_module_text("SCK.Audio.Name");
+}
+
+static void destroy_audio_screen_stream(struct screen_capture *sc)
+{
+    if (sc->disp) {
+        [sc->disp stopCaptureWithCompletionHandler:^(NSError *_Nullable error) {
+            if (error && error.code != 3808) {
+                MACCAP_ERR("destroy_audio_screen_stream: Failed to stop stream with error %s\n",
+                           [[error localizedFailureReason] cStringUsingEncoding:NSUTF8StringEncoding]);
+            }
+            os_event_signal(sc->disp_finished);
+        }];
+        os_event_wait(sc->disp_finished);
+    }
+
+    if (sc->stream_properties) {
+        [sc->stream_properties release];
+        sc->stream_properties = NULL;
+    }
+
+    if (sc->disp) {
+        [sc->disp release];
+        sc->disp = NULL;
+    }
+
+    os_event_destroy(sc->disp_finished);
+    os_event_destroy(sc->stream_start_completed);
+}
+
+static void sck_audio_capture_destroy(void *data)
+{
+    struct screen_capture *sc = data;
+
+    if (!sc)
+        return;
+
+    destroy_audio_screen_stream(sc);
+
+    if (sc->shareable_content) {
+        os_sem_wait(sc->shareable_content_available);
+        [sc->shareable_content release];
+        os_sem_destroy(sc->shareable_content_available);
+        sc->shareable_content_available = NULL;
+    }
+
+    if (sc->capture_delegate) {
+        [sc->capture_delegate release];
+    }
+    [sc->application_id release];
+
+    pthread_mutex_destroy(&sc->mutex);
+    bfree(sc);
+}
+
+static bool init_audio_screen_stream(struct screen_capture *sc)
+{
+    SCContentFilter *content_filter;
+
+    sc->stream_properties = [[SCStreamConfiguration alloc] init];
+    os_sem_wait(sc->shareable_content_available);
+
+    SCDisplay * (^get_target_display)(void) = ^SCDisplay *
+    {
+        for (SCDisplay *display in sc->shareable_content.displays) {
+            if (display.displayID == sc->display) {
+                return display;
+            }
+        }
+        return nil;
+    };
+
+    switch (sc->audio_capture_type) {
+        case ScreenCaptureAudioDesktopStream: {
+            SCDisplay *target_display = get_target_display();
+
+            NSArray *empty = [[NSArray alloc] init];
+            content_filter = [[SCContentFilter alloc] initWithDisplay:target_display excludingWindows:empty];
+            [empty release];
+        } break;
+        case ScreenCaptureAudioApplicationStream: {
+            SCDisplay *target_display = get_target_display();
+            SCRunningApplication *target_application = nil;
+            for (SCRunningApplication *application in sc->shareable_content.applications) {
+                if ([application.bundleIdentifier isEqualToString:sc->application_id]) {
+                    target_application = application;
+                    break;
+                }
+            }
+            NSArray *target_application_array = [[NSArray alloc] initWithObjects:target_application, nil];
+
+            NSArray *empty = [[NSArray alloc] init];
+            content_filter = [[SCContentFilter alloc] initWithDisplay:target_display
+                                                includingApplications:target_application_array
+                                                     exceptingWindows:empty];
+            [target_application_array release];
+            [empty release];
+        } break;
+    }
+    os_sem_post(sc->shareable_content_available);
+    [sc->stream_properties setQueueDepth:8];
+
+    [sc->stream_properties setCapturesAudio:TRUE];
+    [sc->stream_properties setExcludesCurrentProcessAudio:TRUE];
+
+    struct obs_audio_info audio_info;
+    BOOL did_get_audio_info = obs_get_audio_info(&audio_info);
+    if (!did_get_audio_info) {
+        MACCAP_ERR("init_audio_screen_stream: No audio configured, returning %d\n", did_get_audio_info);
+        [content_filter release];
+        return did_get_audio_info;
+    }
+    int channel_count = get_audio_channels(audio_info.speakers);
+    if (channel_count > 1) {
+        [sc->stream_properties setChannelCount:2];
+    } else {
+        [sc->stream_properties setChannelCount:channel_count];
+    }
+
+    sc->disp = [[SCStream alloc] initWithFilter:content_filter configuration:sc->stream_properties
+                                       delegate:sc->capture_delegate];
+    [content_filter release];
+
+    //add a dummy video stream output to silence errors from SCK. frames are dropped by the delegate
+    NSError *error = nil;
+    BOOL did_add_output = [sc->disp addStreamOutput:sc->capture_delegate type:SCStreamOutputTypeScreen
+                                 sampleHandlerQueue:nil
+                                              error:&error];
+    if (!did_add_output) {
+        MACCAP_ERR("init_audio_screen_stream: Failed to add video stream output with error %s\n",
+                   [[error localizedFailureReason] cStringUsingEncoding:NSUTF8StringEncoding]);
+        [error release];
+        [sc->disp release];
+        sc->disp = NULL;
+        return !did_add_output;
+    }
+
+    did_add_output = [sc->disp addStreamOutput:sc->capture_delegate type:SCStreamOutputTypeAudio sampleHandlerQueue:nil
+                                         error:&error];
+    if (!did_add_output) {
+        MACCAP_ERR("init_audio_screen_stream: Failed to add audio stream output with error %s\n",
+                   [[error localizedFailureReason] cStringUsingEncoding:NSUTF8StringEncoding]);
+        [error release];
+        [sc->disp release];
+        sc->disp = NULL;
+        return !did_add_output;
+    }
+    os_event_init(&sc->disp_finished, OS_EVENT_TYPE_MANUAL);
+    os_event_init(&sc->stream_start_completed, OS_EVENT_TYPE_MANUAL);
+
+    __block BOOL did_stream_start = false;
+    [sc->disp startCaptureWithCompletionHandler:^(NSError *_Nullable error2) {
+        did_stream_start = (BOOL) (error2 == nil);
+        if (!did_stream_start) {
+            MACCAP_ERR("init_audio_screen_stream: Failed to start capture with error %s\n",
+                       [[error localizedFailureReason] cStringUsingEncoding:NSUTF8StringEncoding]);
+            // Clean up disp so it isn't stopped
+            [sc->disp release];
+            sc->disp = NULL;
+        }
+        os_event_signal(sc->stream_start_completed);
+    }];
+    os_event_wait(sc->stream_start_completed);
+
+    MACCAP_ERR("init closing, returning %d\n", did_stream_start);
+    return did_stream_start;
+}
+
+static void sck_audio_capture_defaults(obs_data_t *settings)
+{
+    CGDirectDisplayID initial_display = 0;
+    {
+        NSScreen *mainScreen = [NSScreen mainScreen];
+        if (mainScreen) {
+            NSNumber *screen_num = mainScreen.deviceDescription[@"NSScreenNumber"];
+            if (screen_num) {
+                initial_display = (CGDirectDisplayID) (uintptr_t) screen_num.pointerValue;
+            }
+        }
+    }
+
+    CFUUIDRef display_uuid = CGDisplayCreateUUIDFromDisplayID(initial_display);
+    CFStringRef uuid_string = CFUUIDCreateString(kCFAllocatorDefault, display_uuid);
+    obs_data_set_default_string(settings, "display_uuid", CFStringGetCStringPtr(uuid_string, kCFStringEncodingUTF8));
+    CFRelease(uuid_string);
+    CFRelease(display_uuid);
+
+    obs_data_set_default_obj(settings, "application", NULL);
+    obs_data_set_default_int(settings, "type", ScreenCaptureAudioDesktopStream);
+}
+
+static void *sck_audio_capture_create(obs_data_t *settings, obs_source_t *source)
+{
+    struct screen_capture *sc = bzalloc(sizeof(struct screen_capture));
+
+    sc->source = source;
+    sc->audio_only = true;
+    sc->audio_capture_type = (unsigned int) obs_data_get_int(settings, "type");
+
+    os_sem_init(&sc->shareable_content_available, 1);
+    screen_capture_build_content_list(sc, sc->capture_type == ScreenCaptureAudioDesktopStream);
+
+    sc->capture_delegate = [[ScreenCaptureDelegate alloc] init];
+    sc->capture_delegate.sc = sc;
+
+    const char *display_uuid = obs_data_get_string(settings, "display_uuid");
+    if (display_uuid) {
+        CFStringRef uuid_string = CFStringCreateWithCString(kCFAllocatorDefault, display_uuid, kCFStringEncodingUTF8);
+        CFUUIDRef uuid_ref = CFUUIDCreateFromString(kCFAllocatorDefault, uuid_string);
+        sc->display = CGDisplayGetDisplayIDFromUUID(uuid_ref);
+        CFRelease(uuid_string);
+        CFRelease(uuid_ref);
+    } else {
+        sc->display = CGMainDisplayID();
+    }
+
+    sc->application_id = [[NSString alloc] initWithUTF8String:obs_data_get_string(settings, "application")];
+    pthread_mutex_init(&sc->mutex, NULL);
+
+    if (!init_audio_screen_stream(sc))
+        goto fail;
+
+    return sc;
+
+fail:
+    sck_audio_capture_destroy(sc);
+    return NULL;
+}
+
+#pragma mark - obs_properties
+
+static bool audio_capture_method_changed(void *data, obs_properties_t *props, obs_property_t *list __unused,
+                                         obs_data_t *settings)
+{
+    struct screen_capture *sc = data;
+
+    unsigned int capture_type_id = (unsigned int) obs_data_get_int(settings, "type");
+    obs_property_t *app_list = obs_properties_get(props, "application");
+
+    switch (capture_type_id) {
+        case ScreenCaptureAudioDesktopStream: {
+            obs_property_set_visible(app_list, false);
+            break;
+        }
+        case ScreenCaptureAudioApplicationStream: {
+            obs_property_set_visible(app_list, true);
+            screen_capture_build_content_list(sc, capture_type_id == ScreenCaptureAudioDesktopStream);
+            build_application_list(sc, props);
+            break;
+        }
+    }
+
+    return true;
+}
+
+static obs_properties_t *sck_audio_capture_properties(void *data)
+{
+    struct screen_capture *sc = data;
+
+    obs_properties_t *props = obs_properties_create();
+
+    obs_property_t *capture_type = obs_properties_add_list(props, "type", obs_module_text("SCK.Method"),
+                                                           OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);
+
+    obs_property_list_add_int(capture_type, obs_module_text("DesktopAudioCapture"), 0);
+    obs_property_list_add_int(capture_type, obs_module_text("ApplicationAudioCapture"), 1);
+
+    obs_property_set_modified_callback2(capture_type, audio_capture_method_changed, data);
+
+    obs_property_t *app_list = obs_properties_add_list(props, "application", obs_module_text("Application"),
+                                                       OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING);
+
+    if (sc) {
+        switch (sc->audio_capture_type) {
+            case 0: {
+                obs_property_set_visible(app_list, false);
+                break;
+            }
+            case 1: {
+                obs_property_set_visible(app_list, true);
+                break;
+            }
+        }
+    }
+
+    return props;
+}
+
+static void sck_audio_capture_update(void *data, obs_data_t *settings)
+{
+    struct screen_capture *sc = data;
+
+    ScreenCaptureAudioStreamType capture_type = (ScreenCaptureAudioStreamType) obs_data_get_int(settings, "type");
+    NSString *application_id = [[NSString alloc] initWithUTF8String:obs_data_get_string(settings, "application")];
+
+    destroy_audio_screen_stream(sc);
+    sc->audio_capture_type = capture_type;
+    [sc->application_id release];
+    sc->application_id = application_id;
+    init_audio_screen_stream(sc);
+}
+
+#pragma mark - obs_source_info
+
+struct obs_source_info sck_audio_capture_info = {
+    .id = "sck_audio_capture",
+    .type = OBS_SOURCE_TYPE_INPUT,
+    .get_name = sck_audio_capture_getname,
+
+    .create = sck_audio_capture_create,
+    .destroy = sck_audio_capture_destroy,
+
+    .output_flags = OBS_SOURCE_DO_NOT_DUPLICATE | OBS_SOURCE_AUDIO,
+
+    .get_defaults = sck_audio_capture_defaults,
+    .get_properties = sck_audio_capture_properties,
+    .update = sck_audio_capture_update,
+    .icon_type = OBS_ICON_TYPE_AUDIO_OUTPUT,
+};

+ 7 - 0
plugins/mac-capture/mac-sck-common.h

@@ -24,6 +24,11 @@ typedef enum {
     ScreenCaptureApplicationStream = 2,
 } ScreenCaptureStreamType;
 
+typedef enum {
+    ScreenCaptureAudioDesktopStream = 0,
+    ScreenCaptureAudioApplicationStream = 1,
+} ScreenCaptureAudioStreamType;
+
 @interface ScreenCaptureDelegate : NSObject <SCStreamOutput, SCStreamDelegate>
 
 @property struct screen_capture *sc;
@@ -41,6 +46,7 @@ struct screen_capture {
     bool hide_obs;
     bool show_hidden_windows;
     bool show_empty_names;
+    bool audio_only;
 
     SCStream *disp;
     SCStreamConfiguration *stream_properties;
@@ -55,6 +61,7 @@ struct screen_capture {
     pthread_mutex_t mutex;
 
     ScreenCaptureStreamType capture_type;
+    ScreenCaptureAudioStreamType audio_capture_type;
     CGDirectDisplayID display;
     CGWindowID window;
     NSString *application_id;

+ 2 - 2
plugins/mac-capture/mac-sck-common.m

@@ -16,7 +16,7 @@ bool is_screen_capture_available(void)
 - (void)stream:(SCStream *)stream didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer ofType:(SCStreamOutputType)type
 {
     if (self.sc != NULL) {
-        if (type == SCStreamOutputTypeScreen) {
+        if (type == SCStreamOutputTypeScreen && !self.sc->audio_only) {
             screen_stream_video_update(self.sc, sampleBuffer);
         }
 #if __MAC_OS_X_VERSION_MAX_ALLOWED >= 130000
@@ -47,7 +47,7 @@ bool is_screen_capture_available(void)
             break;
     }
 
-    MACCAP_LOG(LOG_WARNING, "%s", error.domain.UTF8String);
+    MACCAP_LOG(LOG_WARNING, "%s", errorMessage.UTF8String);
 }
 
 @end

+ 1 - 0
plugins/mac-capture/mac-sck-video-capture.m

@@ -255,6 +255,7 @@ static void *sck_video_capture_create(obs_data_t *settings, obs_source_t *source
     sc->show_hidden_windows = obs_data_get_bool(settings, "show_hidden_windows");
     sc->window = (CGWindowID) obs_data_get_int(settings, "window");
     sc->capture_type = (unsigned int) obs_data_get_int(settings, "type");
+    sc->audio_only = false;
 
     os_sem_init(&sc->shareable_content_available, 1);
     screen_capture_build_content_list(sc, sc->capture_type == ScreenCaptureDisplayStream);

+ 2 - 0
plugins/mac-capture/plugin-main.c

@@ -27,6 +27,8 @@ bool obs_module_load(void)
 				OBS_SOURCE_DEPRECATED;
 			window_capture_info.output_flags |=
 				OBS_SOURCE_DEPRECATED;
+			extern struct obs_source_info sck_audio_capture_info;
+			obs_register_source(&sck_audio_capture_info);
 		}
 	}
 #endif