From fa2311bc4a3ee04a2a11347a72e387c2c0b65120 Mon Sep 17 00:00:00 2001 From: Palana Date: Sat, 24 Oct 2015 10:38:42 +0200 Subject: [PATCH] mac-avcapture: Add color space/video range properties --- plugins/mac-avcapture/CMakeLists.txt | 1 + plugins/mac-avcapture/av-capture.mm | 424 +++++++++++++++++++- plugins/mac-avcapture/data/locale/en-US.ini | 5 + plugins/mac-avcapture/left-right.hpp | 65 +++ 4 files changed, 478 insertions(+), 17 deletions(-) create mode 100644 plugins/mac-avcapture/left-right.hpp diff --git a/plugins/mac-avcapture/CMakeLists.txt b/plugins/mac-avcapture/CMakeLists.txt index a6cb174af..f3fc11aed 100644 --- a/plugins/mac-avcapture/CMakeLists.txt +++ b/plugins/mac-avcapture/CMakeLists.txt @@ -15,6 +15,7 @@ include_directories(${AVFOUNDATION} ${COCOA}) set(mac-avcapture_HEADERS + left-right.hpp scope-guard.hpp ) diff --git a/plugins/mac-avcapture/av-capture.mm b/plugins/mac-avcapture/av-capture.mm index 9e5129b6a..f7693233e 100644 --- a/plugins/mac-avcapture/av-capture.mm +++ b/plugins/mac-avcapture/av-capture.mm @@ -4,6 +4,7 @@ #import #include +#include #include #include @@ -15,6 +16,7 @@ #include #include +#include "left-right.hpp" #include "scope-guard.hpp" #define NBSP "\xC2\xA0" @@ -49,9 +51,18 @@ struct default_delete { #define TEXT_FRAME_RATE obs_module_text("FrameRate") #define TEXT_MATCH_OBS obs_module_text("MatchOBS") #define TEXT_INPUT_FORMAT obs_module_text("InputFormat") +#define TEXT_COLOR_SPACE obs_module_text("ColorSpace") +#define TEXT_VIDEO_RANGE obs_module_text("VideoRange") +#define TEXT_RANGE_PARTIAL obs_module_text("VideoRange.Partial") +#define TEXT_RANGE_FULL obs_module_text("VideoRange.Full") #define TEXT_AUTO obs_module_text("Auto") +#define TEXT_COLOR_UNKNOWN_NAME "Unknown" +#define TEXT_RANGE_UNKNOWN_NAME "Unknown" + static const FourCharCode INPUT_FORMAT_AUTO = -1; +static const int COLOR_SPACE_AUTO = -1; +static const int VIDEO_RANGE_AUTO = -1; #define MILLI_TIMESCALE 1000 #define MICRO_TIMESCALE (MILLI_TIMESCALE * 1000) @@ -104,6 +115,12 @@ struct observer_handle : {} }; +struct av_video_info { + video_colorspace colorspace; + video_range_type video_range; + bool video_params_valid = false; +}; + } struct av_capture { @@ -113,6 +130,8 @@ struct av_capture { bool device_locked; + left_right::left_right video_info; + AVCaptureVideoDataOutput *out; AVCaptureDevice *device; AVCaptureDeviceInput *device_input; @@ -124,8 +143,10 @@ struct av_capture { FourCharCode fourcc; video_format video_format; - video_colorspace colorspace; - video_range_type video_range; + + bool use_preset = false; + int requested_colorspace = COLOR_SPACE_AUTO; + int requested_video_range = VIDEO_RANGE_AUTO; obs_source_t *source; @@ -250,6 +271,60 @@ struct config_helper { } }; +struct av_capture_ref { + av_capture *capture = nullptr; + OBSSource source; + + av_capture_ref() = default; + + av_capture_ref(av_capture *capture_, obs_weak_source_t *weak_source) + : source(OBSGetStrongRef(weak_source)) + { + if (!source) + return; + + capture = capture_; + } + + operator av_capture *() + { + return capture; + } + + av_capture *operator->() + { + return capture; + } +}; + +struct properties_param { + av_capture *capture = nullptr; + OBSWeakSource weak_source; + + properties_param(av_capture *capture) + : capture(capture) + { + if (!capture) + return; + + weak_source = OBSGetWeakRef(capture->source); + } + + av_capture_ref get_ref() + { + return {capture, weak_source}; + } +}; + +} + +static av_capture_ref get_ref(obs_properties_t *props) +{ + void *param = obs_properties_get_param(props); + if (!param) + return {}; + + return static_cast(param)->get_ref(); } static inline video_format format_from_subtype(FourCharCode subtype) @@ -383,13 +458,45 @@ static inline video_colorspace get_colorspace(CMFormatDescriptionRef desc) static inline bool update_colorspace(av_capture *capture, obs_source_frame *frame, CMFormatDescriptionRef desc, - bool full_range) + bool full_range, av_video_info &vi) { + auto cs_auto = capture->use_preset || + capture->requested_colorspace == COLOR_SPACE_AUTO; + auto vr_auto = capture->use_preset || + capture->requested_video_range == VIDEO_RANGE_AUTO; + video_colorspace colorspace = get_colorspace(desc); video_range_type range = full_range ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; - if (colorspace == capture->colorspace && range == capture->video_range) + + bool cs_matches = false; + if (cs_auto) { + cs_matches = colorspace == vi.colorspace; + } else { + colorspace = static_cast( + capture->requested_colorspace); + cs_matches = colorspace == vi.colorspace; + } + + bool vr_matches = false; + if (vr_auto) { + vr_matches = range == vi.video_range; + } else { + range = static_cast( + capture->requested_video_range); + vr_matches = range == vi.video_range; + full_range = range == VIDEO_RANGE_FULL; + } + + if (cs_matches && vr_matches) { + if (!vi.video_params_valid) + capture->video_info.update([&](av_video_info &vi_) + { + vi_.video_params_valid = + vi.video_params_valid = true; + }); return true; + } frame->full_range = full_range; @@ -399,11 +506,23 @@ static inline bool update_colorspace(av_capture *capture, frame->color_range_max)) { AVLOG(LOG_ERROR, "Failed to get colorspace parameters for " "colorspace %u range %u", colorspace, range); + + if (vi.video_params_valid) + capture->video_info.update([&](av_video_info &vi_) + { + vi_.video_params_valid = + vi.video_params_valid = false; + }); + return false; } - capture->colorspace = colorspace; - capture->video_range = range; + capture->video_info.update([&](av_video_info &vi_) + { + vi_.colorspace = colorspace; + vi_.video_range = range; + vi_.video_params_valid = vi.video_params_valid = true; + }); return true; } @@ -420,6 +539,15 @@ static inline bool update_frame(av_capture *capture, CVImageBufferRef img = CMSampleBufferGetImageBuffer(sample_buffer); + auto vi = capture->video_info.read(); + + bool video_params_were_valid = vi.video_params_valid; + SCOPE_EXIT + { + if (video_params_were_valid != vi.video_params_valid) + obs_source_update_properties(capture->source); + }; + if (format == VIDEO_FORMAT_NONE) { if (capture->fourcc == fourcc) return false; @@ -437,14 +565,23 @@ static inline bool update_frame(av_capture *capture, AV_FOURCC_STR(capture->fourcc), capture->fourcc, AV_FOURCC_STR(fourcc), fourcc); + bool was_yuv = format_is_yuv(frame->format); + capture->fourcc = fourcc; frame->format = format; frame->width = dims.width; frame->height = dims.height; if (format_is_yuv(format) && !update_colorspace(capture, frame, desc, - is_fullrange_yuv(fourcc))) + is_fullrange_yuv(fourcc), vi)) { return false; + } else if (was_yuv == format_is_yuv(format)) { + capture->video_info.update([&](av_video_info &vi_) + { + vi_.video_params_valid = + vi.video_params_valid = true; + }); + } CVPixelBufferLockBaseAddress(img, kCVPixelBufferLock_ReadOnly); @@ -493,8 +630,10 @@ static inline bool update_frame(av_capture *capture, kCMTimeRoundingMethod_Default); frame->timestamp = target_pts_nano.value; - if (!update_frame(capture, frame, sampleBuffer)) + if (!update_frame(capture, frame, sampleBuffer)) { + obs_source_output_video(capture->source, nullptr); return; + } obs_source_output_video(capture->source, frame); @@ -776,6 +915,56 @@ static void find_formats(media_frames_per_second fps, AVCaptureDevice *dev, } } +static bool color_space_valid(int color_space) +{ + switch (color_space) { + case COLOR_SPACE_AUTO: + case VIDEO_CS_DEFAULT: + case VIDEO_CS_601: + case VIDEO_CS_709: + return true; + } + + return false; +} + +static const char *color_space_name(int color_space) +{ + switch (color_space) { + case COLOR_SPACE_AUTO: return "Auto"; + case VIDEO_CS_DEFAULT: return "Default"; + case VIDEO_CS_601: return "CS 601"; + case VIDEO_CS_709: return "CS 709"; + } + + return "Unknown"; +} + +static bool video_range_valid(int video_range) +{ + switch (video_range) { + case VIDEO_RANGE_AUTO: + case VIDEO_RANGE_DEFAULT: + case VIDEO_RANGE_PARTIAL: + case VIDEO_RANGE_FULL: + return true; + } + + return false; +} + +static const char *video_range_name(int video_range) +{ + switch (video_range) { + case VIDEO_RANGE_AUTO: return "Auto"; + case VIDEO_RANGE_DEFAULT: return "Default"; + case VIDEO_RANGE_PARTIAL: return "Partial"; + case VIDEO_RANGE_FULL: return "Full"; + } + + return "Unknown"; +} + static bool init_manual(av_capture *capture, AVCaptureDevice *dev, obs_data_t *settings) { @@ -802,7 +991,23 @@ static bool init_manual(av_capture *capture, AVCaptureDevice *dev, if (refresh) obs_source_update_properties(capture->source); }; - + + capture->requested_colorspace = + obs_data_get_int(settings, "color_space"); + if (!color_space_valid(capture->requested_colorspace)) { + AVLOG(LOG_WARNING, "Unsupported color space: %d", + capture->requested_colorspace); + return false; + } + + capture->requested_video_range = + obs_data_get_int(settings, "video_range"); + if (!video_range_valid(capture->requested_video_range)) { + AVLOG(LOG_WARNING, "Unsupported color range: %d", + capture->requested_video_range); + return false; + } + CMVideoDimensions dims{}; if (!get_resolution(settings, dims)) { AVLOG(LOG_WARNING, "Could not load resolution"); @@ -849,6 +1054,8 @@ static bool init_manual(av_capture *capture, AVCaptureDevice *dev, " FPS: %g (%" PRIu32 "/%" PRIu32 ")\n" " Frame interval: %g" NBSP "s\n" " Input format: %s%s%s (%s)%s\n" + " Requested color space: %s (%d)\n" + " Requested video range: %s (%d)\n" " Using format: %s", dev.localizedName.UTF8String, dev.uniqueID.UTF8String, dims.width, dims.height, @@ -858,6 +1065,10 @@ static bool init_manual(av_capture *capture, AVCaptureDevice *dev, if_name, IF_AUTO(" (actual: "), IF_AUTO(fourcc_subtype_name(actual_format)), AV_FOURCC_STR(actual_format), IF_AUTO(")"), + color_space_name(capture->requested_colorspace), + capture->requested_colorspace, + video_range_name(capture->requested_video_range), + capture->requested_video_range, format.description.UTF8String); #undef IF_AUTO @@ -865,6 +1076,11 @@ static bool init_manual(av_capture *capture, AVCaptureDevice *dev, dev.activeVideoMinFrameDuration = convert(fps); dev.activeVideoMaxFrameDuration = convert(fps); + capture->video_info.update([&](av_video_info &vi) + { + vi.video_params_valid = false; + }); + return true; } @@ -876,7 +1092,7 @@ static void capture_device(av_capture *capture, AVCaptureDevice *dev, obs_data_set_string(settings, "device", dev.uniqueID.UTF8String); AVLOG(LOG_INFO, "Selected device '%s'", name); - if (obs_data_get_bool(settings, "use_preset")) { + if ((capture->use_preset = obs_data_get_bool(settings, "use_preset"))) { if (!init_preset(capture, dev, settings)) return; @@ -1066,6 +1282,8 @@ static void av_capture_defaults(obs_data_t *settings) AVCaptureSessionPreset1280x720.UTF8String); obs_data_set_default_int(settings, "input_format", INPUT_FORMAT_AUTO); + obs_data_set_default_int(settings, "color_space", COLOR_SPACE_AUTO); + obs_data_set_default_int(settings, "video_range", VIDEO_RANGE_AUTO); } static bool update_device_list(obs_property_t *list, @@ -1368,8 +1586,7 @@ static frame_rates_t enumerate_frame_rates(AVCaptureDevice *dev, format.videoSupportedFrameRateRanges) { add_unique_frame_rate_range(range); - //FIXME remove debug true - if (true || CMTimeCompare(range.minFrameDuration, + if (CMTimeCompare(range.minFrameDuration, range.maxFrameDuration) != 0) { blog(LOG_WARNING, "Got actual frame rate range:" " %g - %g " @@ -1543,6 +1760,115 @@ static bool update_input_format_property(obs_properties_t *props, return update_enabled(!formats.empty()); } +static bool update_int_list_property(obs_property_t *p, const int *val, + const size_t count, const char *localization_name) +{ + size_t num = obs_property_list_item_count(p); + if (num > count) { + if (!val || obs_property_list_item_int(p, count) != *val) { + obs_property_list_item_remove(p, count); + + if (!val) + return true; + } else { + return false; + } + } + + if (!val) + return false; + + DStr buf, label; + dstr_printf(buf, "%d", *val); + dstr_init_copy(label, obs_module_text(localization_name)); + dstr_replace(label, "$1", buf->array); + size_t idx = obs_property_list_add_int(p, label->array, *val); + obs_property_list_item_disable(p, idx, true); + + return true; +} + +template +static bool update_int_list_property(const char *prop_name, + const char *localization_name, size_t count, + int auto_val, bool (*valid_func)(int), + obs_properties_t *props, const config_helper &conf, + obs_property_t *p, Func get_val) +{ + auto ref = get_ref(props); + if (!p) + p = obs_properties_get(props, prop_name); + + int val = obs_data_get_int(conf.settings, prop_name); + + av_video_info vi; + if (ref) + vi = ref->video_info.read(); + + bool params_valid = vi.video_params_valid; + bool enabled = obs_property_enabled(p); + bool should_enable = false; + bool has_autoselect = + obs_data_has_autoselect_value(conf.settings, prop_name); + + if ((params_valid && format_is_yuv(ref->frame.format)) || + !valid_func(val)) + should_enable = true; + + obs_property_set_enabled(p, should_enable); + bool updated = enabled != should_enable; + + updated = update_int_list_property(p, + valid_func(val) ? nullptr : &val, + count, localization_name) || updated; + + if (!should_enable) { + if (has_autoselect) + obs_data_unset_autoselect_value(conf.settings, + prop_name); + return updated || has_autoselect; + } + + bool use_autoselect = ref && val == auto_val; + if (!use_autoselect) { + if (has_autoselect) + obs_data_unset_autoselect_value(conf.settings, + prop_name); + return updated || has_autoselect; + } + + if (params_valid && get_val(vi) != + obs_data_get_autoselect_int(conf.settings, prop_name)) { + obs_data_set_autoselect_int(conf.settings, prop_name, + get_val(vi)); + return true; + } + + return updated; +} + +static bool update_color_space_property(obs_properties_t *props, + const config_helper &conf, obs_property_t *p=nullptr) +{ + return update_int_list_property("color_space", TEXT_COLOR_UNKNOWN_NAME, + 4, COLOR_SPACE_AUTO, color_space_valid, props, conf, p, + [](av_video_info vi) + { + return vi.colorspace; + }); +} + +static bool update_video_range_property(obs_properties_t *props, + const config_helper &conf, obs_property_t *p=nullptr) +{ + return update_int_list_property("video_range", TEXT_RANGE_UNKNOWN_NAME, + 5, VIDEO_RANGE_AUTO, video_range_valid, props, conf, p, + [](av_video_info vi) + { + return vi.video_range; + }); +} + static bool properties_device_changed(obs_properties_t *props, obs_property_t *p, obs_data_t *settings) { @@ -1594,6 +1920,8 @@ static bool properties_use_preset_changed(obs_properties_t *props, UPDATE_PROPERTY("resolution", false, update_resolution_property); UPDATE_PROPERTY("frame_rate", false, update_frame_rate_property); UPDATE_PROPERTY("input_format", false, update_input_format_property); + UPDATE_PROPERTY("color_space", false, update_color_space_property); + UPDATE_PROPERTY("video_range", false, update_video_range_property); return updated; } @@ -1618,8 +1946,11 @@ static bool properties_resolution_changed(obs_properties_t *props, bool res_updated = update_resolution_property(props, conf, p); bool fps_updated = update_frame_rate_property(props, conf); bool if_updated = update_input_format_property(props, conf); + bool cs_updated = update_color_space_property(props, conf); + bool cr_updated = update_video_range_property(props, conf); - return res_updated || fps_updated || if_updated; + return res_updated || fps_updated || + if_updated || cs_updated || cr_updated; } static bool properties_frame_rate_changed(obs_properties_t *props, @@ -1629,8 +1960,10 @@ static bool properties_frame_rate_changed(obs_properties_t *props, bool fps_updated = update_frame_rate_property(props, conf, p); bool if_updated = update_input_format_property(props, conf); + bool cs_updated = update_color_space_property(props, conf); + bool cr_updated = update_video_range_property(props, conf); - return fps_updated || if_updated; + return fps_updated || if_updated || cs_updated || cr_updated; } static bool properties_input_format_changed(obs_properties_t *props, @@ -1638,7 +1971,39 @@ static bool properties_input_format_changed(obs_properties_t *props, { config_helper conf{settings}; - return update_input_format_property(props, conf, p); + bool if_updated = update_input_format_property(props, conf, p); + bool cs_updated = update_color_space_property(props, conf); + bool cr_updated = update_video_range_property(props, conf); + + return if_updated || cs_updated || cr_updated; +} + +static bool properties_color_space_changed(obs_properties_t *props, + obs_property_t *p, obs_data_t *settings) +{ + config_helper conf{settings}; + + return update_color_space_property(props, conf, p); +} + +static bool properties_video_range_changed(obs_properties_t *props, + obs_property_t *p, obs_data_t *settings) +{ + config_helper conf{settings}; + + return update_video_range_property(props, conf, p); +} + +static void add_properties_param(obs_properties_t *props, av_capture *capture) +{ + auto param = unique_ptr( + new properties_param(capture)); + + obs_properties_set_param(props, param.release(), + [](void *param) + { + delete static_cast(param); + }); } static void add_preset_properties(obs_properties_t *props) @@ -1680,12 +2045,37 @@ static void add_manual_properties(obs_properties_t *props) obs_property_set_enabled(input_format, false); obs_property_set_modified_callback(input_format, properties_input_format_changed); + + obs_property_t *color_space = obs_properties_add_list(props, + "color_space", TEXT_COLOR_SPACE, + OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); + obs_property_list_add_int(color_space, TEXT_AUTO, COLOR_SPACE_AUTO); + obs_property_list_add_int(color_space, "Rec. 601", VIDEO_CS_601); + obs_property_list_add_int(color_space, "Rec. 709", VIDEO_CS_709); + obs_property_set_enabled(color_space, false); + obs_property_set_modified_callback(color_space, + properties_color_space_changed); + +#define ADD_RANGE(x) \ + obs_property_list_add_int(video_range, TEXT_ ## x, VIDEO_ ## x) + obs_property_t *video_range = obs_properties_add_list(props, + "video_range", TEXT_VIDEO_RANGE, + OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); + obs_property_list_add_int(video_range, TEXT_AUTO, VIDEO_RANGE_AUTO); + ADD_RANGE(RANGE_PARTIAL); + ADD_RANGE(RANGE_FULL); + obs_property_set_enabled(video_range, false); + obs_property_set_modified_callback(video_range, + properties_video_range_changed); +#undef ADD_RANGE } -static obs_properties_t *av_capture_properties(void*) +static obs_properties_t *av_capture_properties(void *capture) { obs_properties_t *props = obs_properties_create(); + add_properties_param(props, static_cast(capture)); + obs_property_t *dev_list = obs_properties_add_list(props, "device", TEXT_DEVICE, OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); @@ -1773,7 +2163,7 @@ static void av_capture_update(void *data, obs_data_t *settings) if (!capture->device || ![capture->device.uniqueID isEqualToString:uid]) return switch_device(capture, uid, settings); - if (obs_data_get_bool(settings, "use_preset")) { + if ((capture->use_preset = obs_data_get_bool(settings, "use_preset"))) { update_preset(capture, settings); } else { update_manual(capture, settings); diff --git a/plugins/mac-avcapture/data/locale/en-US.ini b/plugins/mac-avcapture/data/locale/en-US.ini index 1ffee5a9c..c2d4b71ac 100644 --- a/plugins/mac-avcapture/data/locale/en-US.ini +++ b/plugins/mac-avcapture/data/locale/en-US.ini @@ -5,4 +5,9 @@ Preset="Preset" Buffering="Use Buffering" FrameRate="Frame rate" InputFormat="Input format" +ColorSpace="Color space" +VideoRange="Video range" +VideoRange.Partial="Partial" +VideoRange.Full="Full" Auto="Auto" +Unknown="Unknown ($1)" diff --git a/plugins/mac-avcapture/left-right.hpp b/plugins/mac-avcapture/left-right.hpp new file mode 100644 index 000000000..7b62d30c2 --- /dev/null +++ b/plugins/mac-avcapture/left-right.hpp @@ -0,0 +1,65 @@ +#pragma once + +#include +#include +#include + +#include + +// left_right based on +// https://github.com/pramalhe/ConcurrencyFreaks/blob/master/papers/left-right-2014.pdf +// see concurrencyfreaks.com + +namespace left_right { + +template +struct left_right { + template + void update(Func &&f) + { + std::lock_guard lock(write_mutex); + auto cur = current.load(); + auto next = (cur + 1) & 1; + + while (readers[next] != 0) + std::this_thread::yield(); + + f(data[next]); + current = next; + + while (readers[cur] != 0) + std::this_thread::yield(); + + f(data[cur]); + } + + T read() + { + auto cur = current.load(); + for (;;) { + readers[cur] += 1; + + auto cur_ = current.load(); + if (cur_ == cur) + break; + + readers[cur] -= 1; + cur = cur_; + } + + auto val = data[cur]; + + readers[cur] -= 1; + + return val; + } + +private: + std::atomic_uint_fast8_t current; + std::atomic_long readers[2]; + std::mutex write_mutex; + + T data[2] = {{}, {}}; +}; + +}