obs-studio/plugins/mac-avcapture/plugin-main.m
PatTheMav 03c42e5b93 mac-avcapture: Fix crash issues on Intel-based Macs and older macOS
Fixes several possible crash issues that might occur on Intel-based Macs
and older macOS versions:

On modern macOS versions (13+) allocated memory is zero-allocated by
default which makes NULL pointer checks work correctly after allocation.

On older macOS versions this is not the case, so the OBSAVCaptureInfo
struct needs to be zero-allocated to ensure the guards in the tick and
render functions bail out correctly.

On Intel-based Macs and/or older macOS versions passing a reference to
the OBSAVCapture instance inside the OBSAVCapture struct can lead to a
crash because of a possible circular reference that cannot be resolved
at runtime.

Passing only a reference of the OBSAVCapture to libobs and incrementing
the retain count at source creation (decrementing in when the source is
destroyed) avoids this issue entirely.
2024-02-20 17:57:09 -05:00

310 lines
11 KiB
Objective-C

//
// plugin-main.m
// mac-avcapture
//
// Created by Patrick Heyer on 2023-03-07.
//
#import "plugin-main.h"
#pragma mark av-capture API
const char *av_capture_get_text(const char *text_id)
{
return obs_module_text(text_id);
}
static void *av_capture_create(obs_data_t *settings, obs_source_t *source)
{
OBSAVCaptureInfo *capture_data = bzalloc(sizeof(OBSAVCaptureInfo));
capture_data->isFastPath = false;
capture_data->settings = settings;
capture_data->source = source;
capture_data->videoFrame = bmalloc(sizeof(OBSAVCaptureVideoFrame));
capture_data->audioFrame = bmalloc(sizeof(OBSAVCaptureAudioFrame));
OBSAVCapture *capture = [[OBSAVCapture alloc] initWithCaptureInfo:capture_data];
return (void *) CFBridgingRetain(capture);
}
static void *av_fast_capture_create(obs_data_t *settings, obs_source_t *source)
{
OBSAVCaptureInfo *capture_info = bzalloc(sizeof(OBSAVCaptureInfo));
capture_info->isFastPath = true;
capture_info->settings = settings;
capture_info->source = source;
capture_info->effect = obs_get_base_effect(OBS_EFFECT_DEFAULT_RECT);
capture_info->frameSize = CGRectZero;
if (!capture_info->effect) {
return NULL;
}
pthread_mutex_init(&capture_info->mutex, NULL);
OBSAVCapture *capture = [[OBSAVCapture alloc] initWithCaptureInfo:capture_info];
return (void *) CFBridgingRetain(capture);
}
static const char *av_capture_get_name(void *av_capture __unused)
{
return obs_module_text("AVCapture");
}
static const char *av_fast_capture_get_name(void *av_capture __unused)
{
return obs_module_text("AVCapture_Fast");
}
static void av_capture_set_defaults(obs_data_t *settings)
{
obs_data_set_default_string(settings, "device", "");
obs_data_set_default_bool(settings, "use_preset", true);
obs_data_set_default_string(settings, "preset", AVCaptureSessionPresetHigh.UTF8String);
obs_data_set_default_bool(settings, "enable_audio", true);
}
static void av_fast_capture_set_defaults(obs_data_t *settings)
{
obs_data_set_default_string(settings, "device", "");
obs_data_set_default_bool(settings, "use_preset", false);
obs_data_set_default_bool(settings, "enable_audio", true);
}
static obs_properties_t *av_capture_properties(void *av_capture)
{
OBSAVCapture *capture = (__bridge OBSAVCapture *) (av_capture);
OBSAVCaptureInfo *capture_info = capture.captureInfo;
obs_properties_t *properties = obs_properties_create();
// Create Properties
obs_property_t *device_list = obs_properties_add_list(properties, "device", obs_module_text("Device"),
OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING);
obs_property_t *use_preset = obs_properties_add_bool(properties, "use_preset", obs_module_text("UsePreset"));
obs_property_t *preset_list = obs_properties_add_list(properties, "preset", obs_module_text("Preset"),
OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING);
obs_property_t *resolutions = obs_properties_add_list(properties, "resolution", obs_module_text("Resolution"),
OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING);
obs_property_t *use_buffering = obs_properties_add_bool(properties, "buffering", obs_module_text("Buffering"));
obs_property_t *frame_rates = obs_properties_add_frame_rate(properties, "frame_rate", obs_module_text("FrameRate"));
obs_property_t *input_format = obs_properties_add_list(properties, "input_format", obs_module_text("InputFormat"),
OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);
obs_property_t *color_space = obs_properties_add_list(properties, "color_space", obs_module_text("ColorSpace"),
OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);
obs_property_t *video_range = obs_properties_add_list(properties, "video_range", obs_module_text("VideoRange"),
OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);
if (capture_info) {
bool isFastPath = capture_info->isFastPath;
// Add Property Visibility and Callbacks
configure_property(device_list, true, true, properties_changed, capture);
configure_property(use_preset, !isFastPath, !isFastPath, (!isFastPath) ? properties_changed_use_preset : NULL,
capture);
configure_property(preset_list, !isFastPath, !isFastPath, (!isFastPath) ? properties_changed_preset : NULL,
capture);
configure_property(resolutions, isFastPath, isFastPath, NULL, NULL);
configure_property(use_buffering, !isFastPath, !isFastPath, NULL, NULL);
configure_property(frame_rates, isFastPath, isFastPath, NULL, NULL);
configure_property(color_space, !isFastPath, !isFastPath, NULL, NULL);
configure_property(video_range, !isFastPath, !isFastPath, NULL, NULL);
configure_property(input_format, true, true, NULL, NULL);
}
return properties;
}
static void av_capture_update(void *av_capture, obs_data_t *settings)
{
OBSAVCapture *capture = (__bridge OBSAVCapture *) (av_capture);
capture.captureInfo->settings = settings;
[capture updateSessionwithError:NULL];
}
static void av_fast_capture_tick(void *av_capture, float seconds __unused)
{
OBSAVCapture *capture = (__bridge OBSAVCapture *) (av_capture);
OBSAVCaptureInfo *capture_info = capture.captureInfo;
if (!capture_info->currentSurface) {
return;
}
if (!obs_source_showing(capture_info->source)) {
return;
}
IOSurfaceRef previousSurface = capture_info->previousSurface;
if (pthread_mutex_lock(&capture_info->mutex)) {
return;
}
capture_info->previousSurface = capture_info->currentSurface;
capture_info->currentSurface = NULL;
pthread_mutex_unlock(&capture_info->mutex);
if (previousSurface == capture_info->previousSurface) {
return;
}
if (capture_info->previousSurface) {
obs_enter_graphics();
if (capture_info->texture) {
gs_texture_rebind_iosurface(capture_info->texture, capture_info->previousSurface);
} else {
capture_info->texture = gs_texture_create_from_iosurface(capture_info->previousSurface);
}
obs_leave_graphics();
}
if (previousSurface) {
IOSurfaceDecrementUseCount(previousSurface);
CFRelease(previousSurface);
}
}
static void av_fast_capture_render(void *av_capture, gs_effect_t *effect __unused)
{
OBSAVCapture *capture = (__bridge OBSAVCapture *) (av_capture);
OBSAVCaptureInfo *capture_info = capture.captureInfo;
if (!capture_info->texture) {
return;
}
const bool linear_srgb = gs_get_linear_srgb();
const bool previous = gs_framebuffer_srgb_enabled();
gs_enable_framebuffer_srgb(linear_srgb);
gs_eparam_t *param = gs_effect_get_param_by_name(capture_info->effect, "image");
gs_effect_set_texture_srgb(param, capture_info->texture);
if (linear_srgb) {
gs_effect_set_texture_srgb(param, capture_info->texture);
} else {
gs_effect_set_texture(param, capture_info->texture);
}
while (gs_effect_loop(capture_info->effect, "Draw")) {
gs_draw_sprite(capture_info->texture, 0, 0, 0);
}
gs_enable_framebuffer_srgb(previous);
}
static UInt32 av_fast_capture_get_width(void *av_capture)
{
OBSAVCapture *capture = (__bridge OBSAVCapture *) (av_capture);
OBSAVCaptureInfo *capture_info = capture.captureInfo;
CGSize frameSize = capture_info->frameSize.size;
return (UInt32) frameSize.width;
}
static UInt32 av_fast_capture_get_height(void *av_capture)
{
OBSAVCapture *capture = (__bridge OBSAVCapture *) (av_capture);
OBSAVCaptureInfo *capture_info = capture.captureInfo;
CGSize frameSize = capture_info->frameSize.size;
return (UInt32) frameSize.height;
}
static void av_capture_destroy(void *av_capture)
{
OBSAVCapture *capture = (__bridge OBSAVCapture *) (av_capture);
if (!capture) {
return;
}
OBSAVCaptureInfo *capture_info = capture.captureInfo;
[capture stopCaptureSession];
[capture.deviceInput.device unlockForConfiguration];
if (capture_info->isFastPath) {
pthread_mutex_destroy(&capture_info->mutex);
}
if (capture_info->videoFrame) {
bfree(capture_info->videoFrame);
capture_info->videoFrame = NULL;
}
if (capture_info->audioFrame) {
bfree(capture_info->audioFrame);
capture_info->audioFrame = NULL;
}
if (capture_info->sampleBufferDescription) {
capture_info->sampleBufferDescription = NULL;
}
bfree(capture_info);
CFBridgingRelease((__bridge CFTypeRef _Nullable)(capture));
}
#pragma mark - OBS Module API
OBS_DECLARE_MODULE()
OBS_MODULE_USE_DEFAULT_LOCALE("macOS-avcapture", "en-US")
MODULE_EXPORT const char *obs_module_description(void)
{
return "macOS AVFoundation Capture Source";
}
bool obs_module_load(void)
{
struct obs_source_info av_capture_info = {
.id = "macos-avcapture",
.type = OBS_SOURCE_TYPE_INPUT,
.output_flags = OBS_SOURCE_ASYNC_VIDEO | OBS_SOURCE_AUDIO | OBS_SOURCE_DO_NOT_DUPLICATE,
.create = av_capture_create,
.get_name = av_capture_get_name,
.get_defaults = av_capture_set_defaults,
.get_properties = av_capture_properties,
.update = av_capture_update,
.destroy = av_capture_destroy,
.icon_type = OBS_ICON_TYPE_CAMERA,
};
obs_register_source(&av_capture_info);
struct obs_source_info av_capture_sync_info = {.id = "macos-avcapture-fast",
.type = OBS_SOURCE_TYPE_INPUT,
.output_flags = OBS_SOURCE_VIDEO | OBS_SOURCE_CUSTOM_DRAW |
OBS_SOURCE_AUDIO | OBS_SOURCE_SRGB |
OBS_SOURCE_DO_NOT_DUPLICATE,
.create = av_fast_capture_create,
.get_name = av_fast_capture_get_name,
.get_defaults = av_fast_capture_set_defaults,
.get_properties = av_capture_properties,
.update = av_capture_update,
.destroy = av_capture_destroy,
.video_tick = av_fast_capture_tick,
.video_render = av_fast_capture_render,
.get_width = av_fast_capture_get_width,
.get_height = av_fast_capture_get_height,
.icon_type = OBS_ICON_TYPE_CAMERA
};
obs_register_source(&av_capture_sync_info);
return true;
}