mac-capture: Add macOS Audio Capture

This commit is contained in:
jcm 2023-06-25 16:10:10 -05:00 committed by Patrick Heyer
parent 1152173742
commit 239e9273dc
7 changed files with 341 additions and 4 deletions

View file

@ -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

View file

@ -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"

View file

@ -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,
};

View file

@ -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;

View file

@ -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

View file

@ -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);

View file

@ -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