| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525 |
- //
- // plugin-properties.m
- // mac-avcapture
- //
- // Created by Patrick Heyer on 2023-03-07.
- //
- #import "OBSAVCapture.h"
- #import "plugin-properties.h"
- extern const char *av_capture_get_text(const char *text_id);
- void configure_property(obs_property_t *property, bool enable, bool visible, void *callback, OBSAVCapture *capture)
- {
- if (property) {
- obs_property_set_enabled(property, enable);
- obs_property_set_visible(property, visible);
- if (callback) {
- obs_property_set_modified_callback2(property, callback, (__bridge void *) (capture));
- }
- }
- }
- bool properties_changed(OBSAVCapture *capture, obs_properties_t *properties, obs_property_t *property __unused,
- obs_data_t *settings)
- {
- OBSAVCaptureInfo *captureInfo = capture.captureInfo;
- obs_property_t *prop_use_preset = obs_properties_get(properties, "use_preset");
- obs_property_t *prop_device = obs_properties_get(properties, "device");
- obs_property_t *prop_presets = obs_properties_get(properties, "preset");
- obs_property_set_enabled(prop_use_preset, !captureInfo->isFastPath);
- if (captureInfo && settings) {
- properties_update_device(capture, prop_device, settings);
- bool use_preset = (settings ? obs_data_get_bool(settings, "use_preset") : true);
- if (use_preset) {
- properties_update_preset(capture, prop_presets, settings);
- } else {
- properties_update_config(capture, properties, settings);
- }
- }
- return true;
- }
- bool properties_changed_preset(OBSAVCapture *capture, obs_properties_t *properties __unused, obs_property_t *property,
- obs_data_t *settings)
- {
- bool use_preset = obs_data_get_bool(settings, "use_preset");
- if (capture && settings && use_preset) {
- NSArray *presetKeys =
- [capture.presetList keysSortedByValueUsingComparator:^NSComparisonResult(NSString *obj1, NSString *obj2) {
- NSNumber *obj1Resolution;
- NSNumber *obj2Resolution;
- if ([obj1 isEqualToString:@"High"]) {
- obj1Resolution = @3;
- } else if ([obj1 isEqualToString:@"Medium"]) {
- obj1Resolution = @2;
- } else if ([obj1 isEqualToString:@"Low"]) {
- obj1Resolution = @1;
- } else {
- NSArray<NSString *> *obj1Dimensions = [obj1 componentsSeparatedByString:@"x"];
- obj1Resolution = [NSNumber numberWithInt:([[obj1Dimensions objectAtIndex:0] intValue] *
- [[obj1Dimensions objectAtIndex:1] intValue])];
- }
- if ([obj2 isEqualToString:@"High"]) {
- obj2Resolution = @3;
- } else if ([obj2 isEqualToString:@"Medium"]) {
- obj2Resolution = @2;
- } else if ([obj2 isEqualToString:@"Low"]) {
- obj2Resolution = @1;
- } else {
- NSArray<NSString *> *obj2Dimensions = [obj2 componentsSeparatedByString:@"x"];
- obj2Resolution = [NSNumber numberWithInt:([[obj2Dimensions objectAtIndex:0] intValue] *
- [[obj2Dimensions objectAtIndex:1] intValue])];
- }
- NSComparisonResult result = [obj1Resolution compare:obj2Resolution];
- if (result == NSOrderedAscending) {
- return (NSComparisonResult) NSOrderedDescending;
- } else if (result == NSOrderedDescending) {
- return (NSComparisonResult) NSOrderedAscending;
- } else {
- return (NSComparisonResult) NSOrderedSame;
- }
- }];
- NSString *UUID = [OBSAVCapture stringFromSettings:settings withSetting:@"device"];
- AVCaptureDevice *device = [AVCaptureDevice deviceWithUniqueID:UUID];
- NSString *currentPreset = [OBSAVCapture stringFromSettings:settings withSetting:@"preset"];
- obs_property_list_clear(property);
- if (device) {
- for (NSString *presetName in presetKeys) {
- NSString *presetDescription = capture.presetList[presetName];
- if ([device supportsAVCaptureSessionPreset:presetName]) {
- obs_property_list_add_string(property, presetDescription.UTF8String, presetName.UTF8String);
- } else if ([currentPreset isEqualToString:presetName]) {
- size_t index =
- obs_property_list_add_string(property, presetDescription.UTF8String, presetName.UTF8String);
- obs_property_list_item_disable(property, index, true);
- }
- };
- } else if (UUID.length) {
- size_t index = obs_property_list_add_string(property, capture.presetList[currentPreset].UTF8String,
- currentPreset.UTF8String);
- obs_property_list_item_disable(property, index, true);
- }
- return YES;
- } else {
- return NO;
- }
- }
- bool properties_changed_use_preset(OBSAVCapture *capture, obs_properties_t *properties,
- obs_property_t *property __unused, obs_data_t *settings)
- {
- bool use_preset = obs_data_get_bool(settings, "use_preset");
- obs_property_t *preset_list = obs_properties_get(properties, "preset");
- obs_property_set_visible(preset_list, use_preset);
- if (use_preset) {
- properties_changed_preset(capture, properties, preset_list, settings);
- }
- const char *update_properties[5] = {"resolution", "frame_rate", "color_space", "video_range", "input_format"};
- size_t number_of_properties = sizeof(update_properties) / sizeof(update_properties[0]);
- for (size_t i = 0; i < number_of_properties; i++) {
- obs_property_t *update_property = obs_properties_get(properties, update_properties[i]);
- if (update_property) {
- obs_property_set_visible(update_property, !use_preset);
- obs_property_set_enabled(update_property, !use_preset);
- }
- }
- return true;
- }
- bool properties_update_preset(OBSAVCapture *capture, obs_property_t *property, obs_data_t *settings)
- {
- NSArray *presetKeys =
- [capture.presetList keysSortedByValueUsingComparator:^NSComparisonResult(NSString *obj1, NSString *obj2) {
- NSNumber *obj1Resolution;
- NSNumber *obj2Resolution;
- if ([obj1 isEqualToString:@"High"]) {
- obj1Resolution = @3;
- } else if ([obj1 isEqualToString:@"Medium"]) {
- obj1Resolution = @2;
- } else if ([obj1 isEqualToString:@"Low"]) {
- obj1Resolution = @1;
- } else {
- NSArray<NSString *> *obj1Dimensions = [obj1 componentsSeparatedByString:@"x"];
- obj1Resolution = [NSNumber numberWithInt:([[obj1Dimensions objectAtIndex:0] intValue] *
- [[obj1Dimensions objectAtIndex:1] intValue])];
- }
- if ([obj2 isEqualToString:@"High"]) {
- obj2Resolution = @3;
- } else if ([obj2 isEqualToString:@"Medium"]) {
- obj2Resolution = @2;
- } else if ([obj2 isEqualToString:@"Low"]) {
- obj2Resolution = @1;
- } else {
- NSArray<NSString *> *obj2Dimensions = [obj2 componentsSeparatedByString:@"x"];
- obj2Resolution = [NSNumber numberWithInt:([[obj2Dimensions objectAtIndex:0] intValue] *
- [[obj2Dimensions objectAtIndex:1] intValue])];
- }
- NSComparisonResult result = [obj1Resolution compare:obj2Resolution];
- if (result == NSOrderedAscending) {
- return (NSComparisonResult) NSOrderedDescending;
- } else if (result == NSOrderedDescending) {
- return (NSComparisonResult) NSOrderedAscending;
- } else {
- return (NSComparisonResult) NSOrderedSame;
- }
- }];
- NSString *deviceUUID = [OBSAVCapture stringFromSettings:settings withSetting:@"device"];
- AVCaptureDevice *device = [AVCaptureDevice deviceWithUniqueID:deviceUUID];
- NSString *currentPreset = [OBSAVCapture stringFromSettings:settings withSetting:@"preset"];
- obs_property_list_clear(property);
- if (device) {
- for (NSString *presetName in presetKeys) {
- NSString *presetDescription = capture.presetList[presetName];
- if ([device supportsAVCaptureSessionPreset:presetName]) {
- obs_property_list_add_string(property, presetDescription.UTF8String, presetName.UTF8String);
- } else if ([currentPreset isEqualToString:presetName]) {
- size_t index =
- obs_property_list_add_string(property, presetDescription.UTF8String, presetName.UTF8String);
- obs_property_list_item_disable(property, index, true);
- }
- };
- } else if (deviceUUID.length) {
- size_t index = obs_property_list_add_string(property, capture.presetList[currentPreset].UTF8String,
- currentPreset.UTF8String);
- obs_property_list_item_disable(property, index, true);
- }
- return true;
- }
- bool properties_update_device(OBSAVCapture *capture __unused, obs_property_t *property, obs_data_t *settings)
- {
- obs_property_list_clear(property);
- NSString *currentDeviceUUID = [OBSAVCapture stringFromSettings:settings withSetting:@"device"];
- NSString *currentDeviceName = [OBSAVCapture stringFromSettings:settings withSetting:@"device_name"];
- BOOL isDeviceFound = NO;
- obs_property_list_add_string(property, "", "");
- NSArray *deviceTypes;
- if (@available(macOS 13, *)) {
- deviceTypes = @[
- AVCaptureDeviceTypeBuiltInWideAngleCamera, AVCaptureDeviceTypeExternalUnknown,
- AVCaptureDeviceTypeDeskViewCamera
- ];
- } else {
- deviceTypes = @[AVCaptureDeviceTypeBuiltInWideAngleCamera, AVCaptureDeviceTypeExternalUnknown];
- }
- AVCaptureDeviceDiscoverySession *videoDiscoverySession =
- [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:deviceTypes mediaType:AVMediaTypeVideo
- position:AVCaptureDevicePositionUnspecified];
- AVCaptureDeviceDiscoverySession *muxedDiscoverySession =
- [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:deviceTypes mediaType:AVMediaTypeMuxed
- position:AVCaptureDevicePositionUnspecified];
- for (AVCaptureDevice *device in [videoDiscoverySession devices]) {
- obs_property_list_add_string(property, device.localizedName.UTF8String, device.uniqueID.UTF8String);
- if (!isDeviceFound && [currentDeviceUUID isEqualToString:device.uniqueID]) {
- isDeviceFound = YES;
- }
- }
- for (AVCaptureDevice *device in [muxedDiscoverySession devices]) {
- obs_property_list_add_string(property, device.localizedName.UTF8String, device.uniqueID.UTF8String);
- if (!isDeviceFound && [currentDeviceUUID isEqualToString:device.uniqueID]) {
- isDeviceFound = YES;
- }
- }
- if (!isDeviceFound && currentDeviceUUID.length > 0) {
- size_t index =
- obs_property_list_add_string(property, currentDeviceName.UTF8String, currentDeviceUUID.UTF8String);
- obs_property_list_item_disable(property, index, true);
- }
- return true;
- }
- bool properties_update_config(OBSAVCapture *capture, obs_properties_t *properties, obs_data_t *settings)
- {
- AVCaptureDevice *device = [AVCaptureDevice deviceWithUniqueID:[OBSAVCapture stringFromSettings:settings
- withSetting:@"device"]];
- obs_property_t *prop_resolution = obs_properties_get(properties, "resolution");
- obs_property_t *prop_framerate = obs_properties_get(properties, "frame_rate");
- obs_property_list_clear(prop_resolution);
- obs_property_frame_rate_clear(prop_framerate);
- obs_property_t *prop_input_format = NULL;
- obs_property_t *prop_color_space = NULL;
- obs_property_t *prop_video_range = NULL;
- prop_input_format = obs_properties_get(properties, "input_format");
- obs_property_list_clear(prop_input_format);
- if (!capture.isFastPath) {
- prop_color_space = obs_properties_get(properties, "color_space");
- prop_video_range = obs_properties_get(properties, "video_range");
- obs_property_list_clear(prop_video_range);
- obs_property_list_clear(prop_color_space);
- }
- CMVideoDimensions resolution = [OBSAVCapture dimensionsFromSettings:settings];
- if (resolution.width == 0 || resolution.height == 0) {
- [capture AVCaptureLog:LOG_DEBUG withFormat:@"No valid resolution found in settings"];
- }
- struct media_frames_per_second fps;
- if (!obs_data_get_frames_per_second(settings, "frame_rate", &fps, NULL)) {
- [capture AVCaptureLog:LOG_DEBUG withFormat:@"No valid framerate found in settings"];
- }
- CMTime time = {.value = fps.denominator, .timescale = fps.numerator, .flags = 1};
- int input_format = 0;
- int color_space = 0;
- int video_range = 0;
- NSMutableArray *inputFormats = NULL;
- NSMutableArray *colorSpaces = NULL;
- NSMutableArray *videoRanges = NULL;
- input_format = (int) obs_data_get_int(settings, "input_format");
- inputFormats = [[NSMutableArray alloc] init];
- if (!capture.isFastPath) {
- color_space = (int) obs_data_get_int(settings, "color_space");
- video_range = (int) obs_data_get_int(settings, "video_range");
- colorSpaces = [[NSMutableArray alloc] init];
- videoRanges = [[NSMutableArray alloc] init];
- }
- NSMutableArray *resolutions = [[NSMutableArray alloc] init];
- NSMutableArray *frameRates = [[NSMutableArray alloc] init];
- BOOL hasFoundResolution = NO;
- BOOL hasFoundFramerate = NO;
- BOOL hasFoundInputFormat = NO;
- BOOL hasFoundColorSpace = capture.isFastPath;
- BOOL hasFoundVideoRange = capture.isFastPath;
- CFPropertyListRef priorColorPrimary = @"";
- if (device) {
- // Iterate over all formats reported by the device and gather them for property lists
- for (AVCaptureDeviceFormat *format in device.formats) {
- if (!capture.isFastPath) {
- FourCharCode formatSubType = CMFormatDescriptionGetMediaSubType(format.formatDescription);
- NSString *formatDescription = [OBSAVCapture stringFromSubType:formatSubType];
- int device_format = [OBSAVCapture formatFromSubtype:formatSubType];
- int device_range;
- const char *range_description;
- if ([OBSAVCapture isFullRangeFormat:formatSubType]) {
- device_range = VIDEO_RANGE_FULL;
- range_description = av_capture_get_text("VideoRange.Full");
- } else {
- device_range = VIDEO_RANGE_PARTIAL;
- range_description = av_capture_get_text("VideoRange.Partial");
- }
- if (!hasFoundInputFormat && input_format == device_format) {
- hasFoundInputFormat = YES;
- }
- if (!hasFoundVideoRange && video_range == device_range) {
- hasFoundVideoRange = YES;
- }
- if (![inputFormats containsObject:@(formatSubType)]) {
- obs_property_list_add_int(prop_input_format, formatDescription.UTF8String, device_format);
- [inputFormats addObject:@(formatSubType)];
- }
- if (![videoRanges containsObject:@(range_description)]) {
- obs_property_list_add_int(prop_video_range, range_description, device_range);
- [videoRanges addObject:@(range_description)];
- }
- int device_color_space = [OBSAVCapture colorspaceFromDescription:format.formatDescription];
- if (![colorSpaces containsObject:@(device_color_space)]) {
- obs_property_list_add_int(prop_color_space,
- [OBSAVCapture stringFromColorspace:device_color_space].UTF8String,
- device_color_space);
- [colorSpaces addObject:@(device_color_space)];
- }
- if (!hasFoundColorSpace && device_color_space == color_space) {
- hasFoundColorSpace = YES;
- }
- } else {
- FourCharCode formatSubType = CMFormatDescriptionGetMediaSubType(format.formatDescription);
- NSString *formatDescription = [OBSAVCapture stringFromSubType:formatSubType];
- int device_format = [OBSAVCapture formatFromSubtype:formatSubType];
- if (!hasFoundInputFormat && input_format == device_format) {
- hasFoundInputFormat = YES;
- }
- if (![inputFormats containsObject:@(formatSubType)]) {
- obs_property_list_add_int(prop_input_format, formatDescription.UTF8String, device_format);
- [inputFormats addObject:@(formatSubType)];
- }
- }
- CMVideoDimensions formatDimensions = CMVideoFormatDescriptionGetDimensions(format.formatDescription);
- NSDictionary *resolutionData =
- @{@"width": @(formatDimensions.width),
- @"height": @(formatDimensions.height)};
- if (![resolutions containsObject:resolutionData]) {
- [resolutions addObject:resolutionData];
- }
- if (!hasFoundResolution && formatDimensions.width == resolution.width &&
- formatDimensions.height == resolution.height) {
- hasFoundResolution = YES;
- }
- // Only iterate over available framerates if input format, color space, and resolution are matching
- if (hasFoundInputFormat && hasFoundColorSpace && hasFoundResolution) {
- CFComparisonResult isColorPrimaryMatch = kCFCompareEqualTo;
- CFPropertyListRef colorPrimary = CMFormatDescriptionGetExtension(
- format.formatDescription, kCMFormatDescriptionExtension_ColorPrimaries);
- if (colorPrimary) {
- isColorPrimaryMatch = CFStringCompare(colorPrimary, priorColorPrimary, 0);
- }
- if (isColorPrimaryMatch != kCFCompareEqualTo || !hasFoundFramerate) {
- for (AVFrameRateRange *range in format.videoSupportedFrameRateRanges.reverseObjectEnumerator) {
- FourCharCode formatSubType = CMFormatDescriptionGetMediaSubType(format.formatDescription);
- int device_format = [OBSAVCapture formatFromSubtype:formatSubType];
- if (input_format == device_format) {
- struct media_frames_per_second min_fps = {
- .numerator = (uint32_t) clamp_Uint(range.maxFrameDuration.timescale, 0, UINT32_MAX),
- .denominator = (uint32_t) clamp_Uint(range.maxFrameDuration.value, 0, UINT32_MAX)};
- struct media_frames_per_second max_fps = {
- .numerator = (uint32_t) clamp_Uint(range.minFrameDuration.timescale, 0, UINT32_MAX),
- .denominator = (uint32_t) clamp_Uint(range.minFrameDuration.value, 0, UINT32_MAX)};
- if (![frameRates containsObject:range]) {
- obs_property_frame_rate_fps_range_add(prop_framerate, min_fps, max_fps);
- [frameRates addObject:range];
- }
- if (!hasFoundFramerate && CMTimeCompare(range.maxFrameDuration, time) >= 0 &&
- CMTimeCompare(range.minFrameDuration, time) <= 0) {
- hasFoundFramerate = YES;
- }
- }
- }
- priorColorPrimary = colorPrimary;
- }
- }
- }
- // Add resolutions in reverse order (formats reported by macOS are sorted with lowest resolution first)
- for (NSDictionary *resolutionData in resolutions.reverseObjectEnumerator) {
- NSError *error;
- NSData *jsonData = [NSJSONSerialization dataWithJSONObject:resolutionData options:0 error:&error];
- int width = [[resolutionData objectForKey:@"width"] intValue];
- int height = [[resolutionData objectForKey:@"height"] intValue];
- obs_property_list_add_string(
- prop_resolution, [NSString stringWithFormat:@"%dx%d", width, height].UTF8String,
- [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding].UTF8String);
- }
- // Add currently selected values in disabled state if they are not supported by the device
- size_t index;
- FourCharCode formatSubType = [OBSAVCapture fourCharCodeFromFormat:input_format withRange:video_range];
- if (!hasFoundInputFormat) {
- NSString *formatDescription = [OBSAVCapture stringFromSubType:formatSubType];
- index = obs_property_list_add_int(prop_input_format, formatDescription.UTF8String, input_format);
- obs_property_list_item_disable(prop_input_format, index, true);
- }
- if (!capture.isFastPath) {
- if (!hasFoundVideoRange) {
- int device_range;
- const char *range_description;
- if ([OBSAVCapture isFullRangeFormat:formatSubType]) {
- device_range = VIDEO_RANGE_FULL;
- range_description = av_capture_get_text("VideoRange.Full");
- } else {
- device_range = VIDEO_RANGE_PARTIAL;
- range_description = av_capture_get_text("VideoRange.Partial");
- }
- index = obs_property_list_add_int(prop_video_range, range_description, device_range);
- obs_property_list_item_disable(prop_video_range, index, true);
- }
- if (!hasFoundColorSpace) {
- index = obs_property_list_add_int(
- prop_color_space, [OBSAVCapture stringFromColorspace:color_space].UTF8String, color_space);
- obs_property_list_item_disable(prop_color_space, index, true);
- }
- }
- if (!hasFoundResolution) {
- NSDictionary *resolutionData = @{@"width": @(resolution.width), @"height": @(resolution.height)};
- NSError *error;
- NSData *jsonData = [NSJSONSerialization dataWithJSONObject:resolutionData options:0 error:&error];
- index = obs_property_list_add_string(
- prop_resolution, [NSString stringWithFormat:@"%dx%d", resolution.width, resolution.height].UTF8String,
- [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding].UTF8String);
- obs_property_list_item_disable(prop_resolution, index, true);
- }
- }
- return true;
- }
|