From 1fdcb9b477b6396980cd827f1a6f438837b2e5c3 Mon Sep 17 00:00:00 2001 From: tytan652 Date: Sat, 18 Jan 2025 13:55:01 +0100 Subject: [PATCH] linux-pipewire: Use list-based format selector for video capture Negotiation is now made with the user selected format. Also fixes a typo in the name of a structure. --- plugins/linux-pipewire/camera-portal.c | 260 +++++++++---------- plugins/linux-pipewire/data/locale/en-US.ini | 1 - plugins/linux-pipewire/pipewire.c | 31 ++- plugins/linux-pipewire/pipewire.h | 6 +- plugins/linux-pipewire/screencast-portal.c | 4 +- 5 files changed, 146 insertions(+), 156 deletions(-) diff --git a/plugins/linux-pipewire/camera-portal.c b/plugins/linux-pipewire/camera-portal.c index ba730a8df1a94c..6c07efbe5a2b9e 100644 --- a/plugins/linux-pipewire/camera-portal.c +++ b/plugins/linux-pipewire/camera-portal.c @@ -44,6 +44,11 @@ struct camera_portal_source { obs_pipewire_stream *obs_pw_stream; char *device_id; + bool restart_stream; + + enum spa_media_subtype subtype; + struct obs_pw_video_format format; + struct { struct spa_rectangle rect; bool set; @@ -242,7 +247,7 @@ static bool update_device_id(struct camera_portal_source *camera_source, const c static void stream_camera(struct camera_portal_source *camera_source) { - struct obs_pipwire_connect_stream_info connect_info; + struct obs_pipewire_connect_stream_info connect_info; const struct spa_rectangle *resolution = NULL; const struct spa_fraction *framerate = NULL; struct camera_device *device; @@ -261,11 +266,13 @@ static void stream_camera(struct camera_portal_source *camera_source) if (camera_source->framerate.set) framerate = &camera_source->framerate.fraction; - connect_info = (struct obs_pipwire_connect_stream_info){ + connect_info = (struct obs_pipewire_connect_stream_info){ .stream_name = "OBS PipeWire Camera", .stream_properties = pw_properties_new(PW_KEY_MEDIA_TYPE, "Video", PW_KEY_MEDIA_CATEGORY, "Capture", PW_KEY_MEDIA_ROLE, "Camera", NULL), .video = { + .subtype = &camera_source->subtype, + .format = &camera_source->format, .resolution = resolution, .framerate = framerate, }}; @@ -277,14 +284,17 @@ static void stream_camera(struct camera_portal_source *camera_source) static void camera_format_list(struct camera_device *dev, obs_property_t *prop) { struct param *p; - enum video_format last_format = VIDEO_FORMAT_NONE; + obs_data_t *data = NULL; obs_property_list_clear(prop); spa_list_for_each(p, &dev->param_list, link) { - struct obs_pw_video_format obs_pw_video_format; - uint32_t media_type, media_subtype, format; + struct dstr str = {}; + uint32_t media_type, media_subtype; + uint32_t format = 0; + const char *format_name; + struct spa_rectangle resolution; if (p->id != SPA_PARAM_EnumFormat || p->param == NULL) continue; @@ -293,24 +303,44 @@ static void camera_format_list(struct camera_device *dev, obs_property_t *prop) continue; if (media_type != SPA_MEDIA_TYPE_video) continue; + + g_clear_pointer(&data, obs_data_release); + + data = obs_data_create(); + if (media_subtype == SPA_MEDIA_SUBTYPE_raw) { + struct obs_pw_video_format obs_pw_video_format; + if (spa_pod_parse_object(p->param, SPA_TYPE_OBJECT_Format, NULL, SPA_FORMAT_VIDEO_format, SPA_POD_Id(&format)) < 0) continue; - } else { - format = SPA_VIDEO_FORMAT_ENCODED; - } - if (!obs_pw_video_format_from_spa_format(format, &obs_pw_video_format)) + if (!obs_pw_video_format_from_spa_format(format, &obs_pw_video_format)) + if (!obs_pw_video_format_from_spa_format(format, &obs_pw_video_format)) + if (!obs_pw_video_format_from_spa_format(format, &obs_pw_video_format)) + continue; + + obs_data_set_bool(data, "encoded", false); + obs_data_set_int(data, "video_format", format); + + format_name = obs_pw_video_format.pretty_name; + } else { continue; + } - if (obs_pw_video_format.video_format == last_format) + if (spa_pod_parse_object(p->param, SPA_TYPE_OBJECT_Format, format ? &format : NULL, + SPA_FORMAT_VIDEO_size, SPA_POD_OPT_Rectangle(&resolution)) < 0) continue; - last_format = obs_pw_video_format.video_format; + obs_data_set_int(data, "width", resolution.width); + obs_data_set_int(data, "height", resolution.height); - obs_property_list_add_int(prop, obs_pw_video_format.pretty_name, format); + dstr_printf(&str, "%ux%u - %s", resolution.width, resolution.height, format_name); + obs_property_list_add_string(prop, str.array, obs_data_get_json(data)); + dstr_free(&str); } + + g_clear_pointer(&data, obs_data_release); } static bool control_changed(void *data, obs_properties_t *props, obs_property_t *prop, obs_data_t *settings) @@ -479,12 +509,11 @@ static bool device_selected(void *data, obs_properties_t *props, obs_property_t if (device == NULL) return false; - if (update_device_id(camera_source, device_id)) - stream_camera(camera_source); + camera_source->restart_stream = update_device_id(camera_source, device_id); blog(LOG_INFO, "[camera-portal] Updating pixel formats"); - property = obs_properties_get(props, "pixelformat"); + property = obs_properties_get(props, "format"); new_control_properties = obs_properties_create(); obs_properties_remove_by_name(props, "controls"); @@ -499,108 +528,6 @@ static bool device_selected(void *data, obs_properties_t *props, obs_property_t return true; } -static int sort_resolutions(gconstpointer a, gconstpointer b) -{ - const struct spa_rectangle *resolution_a = a; - const struct spa_rectangle *resolution_b = b; - int64_t area_a = resolution_a->width * resolution_a->height; - int64_t area_b = resolution_b->width * resolution_b->height; - - return area_a - area_b; -} - -static void resolution_list(struct camera_device *dev, uint32_t pixelformat, obs_property_t *prop) -{ - struct spa_rectangle last_resolution = SPA_RECTANGLE(0, 0); - g_autoptr(GArray) resolutions = NULL; - struct param *p; - obs_data_t *data; - - resolutions = g_array_new(FALSE, FALSE, sizeof(struct spa_rectangle)); - - spa_list_for_each(p, &dev->param_list, link) - { - struct obs_pw_video_format obs_pw_video_format; - struct spa_rectangle resolution; - uint32_t media_type, media_subtype, format; - - if (p->id != SPA_PARAM_EnumFormat || p->param == NULL) - continue; - - if (spa_format_parse(p->param, &media_type, &media_subtype) < 0) - continue; - if (media_type != SPA_MEDIA_TYPE_video) - continue; - if (media_subtype == SPA_MEDIA_SUBTYPE_raw) { - if (spa_pod_parse_object(p->param, SPA_TYPE_OBJECT_Format, NULL, SPA_FORMAT_VIDEO_format, - SPA_POD_Id(&format)) < 0) - continue; - } else { - format = SPA_VIDEO_FORMAT_ENCODED; - } - - if (!obs_pw_video_format_from_spa_format(format, &obs_pw_video_format)) - continue; - - if (obs_pw_video_format.video_format != pixelformat) - continue; - - if (spa_pod_parse_object(p->param, SPA_TYPE_OBJECT_Format, NULL, SPA_FORMAT_VIDEO_size, - SPA_POD_OPT_Rectangle(&resolution)) < 0) - continue; - - if (resolution.width == last_resolution.width && resolution.height == last_resolution.height) - continue; - - last_resolution = resolution; - g_array_append_val(resolutions, resolution); - } - - g_array_sort(resolutions, sort_resolutions); - - obs_property_list_clear(prop); - - data = obs_data_create(); - for (size_t i = 0; i < resolutions->len; i++) { - const struct spa_rectangle *resolution = &g_array_index(resolutions, struct spa_rectangle, i); - struct dstr str = {}; - - dstr_printf(&str, "%ux%u", resolution->width, resolution->height); - - obs_data_set_int(data, "width", resolution->width); - obs_data_set_int(data, "height", resolution->height); - - obs_property_list_add_string(prop, str.array, obs_data_get_json(data)); - - dstr_free(&str); - } - obs_data_release(data); -} - -/* - * Format selected callback - */ -static bool format_selected(void *data, obs_properties_t *properties, obs_property_t *property, obs_data_t *settings) -{ - UNUSED_PARAMETER(property); - UNUSED_PARAMETER(settings); - - struct camera_portal_source *camera_source = data; - struct camera_device *device; - obs_property_t *resolution; - - blog(LOG_INFO, "[camera-portal] Selected format for '%s'", camera_source->device_id); - - device = g_hash_table_lookup(connection->devices, camera_source->device_id); - if (device == NULL) - return false; - - resolution = obs_properties_get(properties, "resolution"); - resolution_list(device, obs_data_get_int(settings, "pixelformat"), resolution); - - return true; -} - static int compare_framerates(gconstpointer a, gconstpointer b) { const struct spa_fraction *framerate_a = a; @@ -760,47 +687,68 @@ static bool framerate_selected(void *data, obs_properties_t *properties, obs_pro } /* - * Resolution selected callback + * Format selected callback */ - -static bool parse_resolution(struct spa_rectangle *dest, const char *json) +static bool parse_format(struct camera_portal_source *dest, const char *json) { obs_data_t *data = obs_data_create_from_json(json); + bool ret = false; if (!data) return false; - dest->width = obs_data_get_int(data, "width"); - dest->height = obs_data_get_int(data, "height"); + if (obs_data_has_user_value(data, "video_format") && obs_data_has_user_value(data, "encoded")) { + struct obs_pw_video_format format; + + if (obs_data_get_bool(data, "encoded")) { + dest->subtype = obs_data_get_int(data, "video_format"); + ret = true; + } else if (obs_pw_video_format_from_spa_format(obs_data_get_int(data, "video_format"), &format)) { + dest->subtype = SPA_MEDIA_SUBTYPE_raw; + dest->format = format; + ret = true; + } + + if (obs_data_has_user_value(data, "width") && obs_data_has_user_value(data, "height")) { + dest->resolution.rect.width = obs_data_get_int(data, "width"); + dest->resolution.rect.height = obs_data_get_int(data, "height"); + dest->resolution.set = true; + } + } + obs_data_release(data); - return true; + return ret; } -static bool resolution_selected(void *data, obs_properties_t *properties, obs_property_t *property, - obs_data_t *settings) +static bool format_selected(void *data, obs_properties_t *properties, obs_property_t *property, obs_data_t *settings) { - UNUSED_PARAMETER(properties); UNUSED_PARAMETER(property); UNUSED_PARAMETER(settings); struct camera_portal_source *camera_source = data; - struct spa_rectangle resolution; struct camera_device *device; + enum spa_media_subtype last_subtype = camera_source->subtype; + enum spa_video_format last_format = camera_source->format.spa_format; - blog(LOG_INFO, "[camera-portal] Selected resolution for '%s'", camera_source->device_id); + blog(LOG_INFO, "[camera-portal] Selected format for '%s'", camera_source->device_id); device = g_hash_table_lookup(connection->devices, camera_source->device_id); if (device == NULL) return false; - if (!parse_resolution(&resolution, obs_data_get_string(settings, "resolution"))) + if (!parse_format(camera_source, obs_data_get_string(settings, "format"))) return false; - if (camera_source->obs_pw_stream) - obs_pipewire_stream_set_resolution(camera_source->obs_pw_stream, &resolution); + if (!camera_source->obs_pw_stream || camera_source->restart_stream || last_subtype != camera_source->subtype || + last_format != camera_source->format.spa_format) { + camera_source->restart_stream = false; + stream_camera(camera_source); + } else if (camera_source->obs_pw_stream) { + obs_pipewire_stream_set_resolution(camera_source->obs_pw_stream, &camera_source->resolution.rect); + } property = obs_properties_get(properties, "framerate"); - framerate_list(device, obs_data_get_int(settings, "pixelformat"), &resolution, property); + framerate_list(device, camera_source->format.spa_format, &camera_source->resolution.rect, property); return true; } @@ -1116,6 +1064,19 @@ static const char *pipewire_camera_get_name(void *data) return obs_module_text("PipeWireCamera"); } +static bool parse_resolution(struct spa_rectangle *dest, const char *json) +{ + obs_data_t *data = obs_data_create_from_json(json); + + if (!data) + return false; + + dest->width = obs_data_get_int(data, "width"); + dest->height = obs_data_get_int(data, "height"); + obs_data_release(data); + return true; +} + static void *pipewire_camera_create(obs_data_t *settings, obs_source_t *source) { struct camera_portal_source *camera_source; @@ -1125,8 +1086,30 @@ static void *pipewire_camera_create(obs_data_t *settings, obs_source_t *source) camera_source->device_id = bstrdup(obs_data_get_string(settings, "device_id")); camera_source->framerate.set = parse_framerate(&camera_source->framerate.fraction, obs_data_get_string(settings, "framerate")); - camera_source->resolution.set = - parse_resolution(&camera_source->resolution.rect, obs_data_get_string(settings, "resolution")); + + if (obs_data_has_user_value(settings, "format")) { + parse_format(camera_source, obs_data_get_string(settings, "format")); + } else if (obs_pw_video_format_from_spa_format(obs_data_get_int(settings, "pixelformat"), + &camera_source->format)) { + camera_source->subtype = SPA_MEDIA_SUBTYPE_raw; + + camera_source->resolution.set = + parse_resolution(&camera_source->resolution.rect, obs_data_get_string(settings, "resolution")); + + /* NOTE: We can convert to the new format only if resolution is available */ + if (camera_source->resolution.set) { + obs_data_t *format = obs_data_create(); + + obs_data_set_bool(format, "encoded", false); + obs_data_set_int(format, "video_format", camera_source->format.spa_format); + obs_data_set_int(format, "width", camera_source->resolution.rect.width); + obs_data_set_int(format, "height", camera_source->resolution.rect.height); + + obs_data_set_string(settings, "format", obs_data_get_json(format)); + + obs_data_release(format); + } + } access_camera(camera_source); @@ -1156,7 +1139,6 @@ static obs_properties_t *pipewire_camera_get_properties(void *data) struct camera_portal_source *camera_source = data; obs_properties_t *controls_props; obs_properties_t *props; - obs_property_t *resolution_list; obs_property_t *framerate_list; obs_property_t *device_list; obs_property_t *format_list; @@ -1166,11 +1148,8 @@ static obs_properties_t *pipewire_camera_get_properties(void *data) device_list = obs_properties_add_list(props, "device_id", obs_module_text("PipeWireCameraDevice"), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); - format_list = obs_properties_add_list(props, "pixelformat", obs_module_text("VideoFormat"), OBS_COMBO_TYPE_LIST, - OBS_COMBO_FORMAT_INT); - - resolution_list = obs_properties_add_list(props, "resolution", obs_module_text("Resolution"), - OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); + format_list = obs_properties_add_list(props, "format", obs_module_text("VideoFormat"), OBS_COMBO_TYPE_LIST, + OBS_COMBO_FORMAT_STRING); framerate_list = obs_properties_add_list(props, "framerate", obs_module_text("FrameRate"), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); @@ -1184,7 +1163,6 @@ static obs_properties_t *pipewire_camera_get_properties(void *data) obs_property_set_modified_callback2(device_list, device_selected, camera_source); obs_property_set_modified_callback2(format_list, format_selected, camera_source); - obs_property_set_modified_callback2(resolution_list, resolution_selected, camera_source); obs_property_set_modified_callback2(framerate_list, framerate_selected, camera_source); return props; diff --git a/plugins/linux-pipewire/data/locale/en-US.ini b/plugins/linux-pipewire/data/locale/en-US.ini index f7482bc5401c25..a7eedd13a03577 100644 --- a/plugins/linux-pipewire/data/locale/en-US.ini +++ b/plugins/linux-pipewire/data/locale/en-US.ini @@ -7,6 +7,5 @@ PipeWireSelectMonitor="Select Monitor" PipeWireSelectWindow="Select Window" PipeWireWindowCapture="Window Capture (PipeWire)" PipeWireSelectScreenCast="Open Selector" -Resolution="Resolution" ShowCursor="Show Cursor" VideoFormat="Video Format" diff --git a/plugins/linux-pipewire/pipewire.c b/plugins/linux-pipewire/pipewire.c index 5ac93d246e4fda..f734a8d56c7f23 100644 --- a/plugins/linux-pipewire/pipewire.c +++ b/plugins/linux-pipewire/pipewire.c @@ -474,17 +474,28 @@ static void init_format_info_sync(obs_pipewire_stream *obs_pw_stream) bfree(drm_formats); } -static void init_format_info(obs_pipewire_stream *obs_pw_stream) +static void init_format_info(obs_pipewire_stream *obs_pw_stream, const struct obs_pw_video_format *selected_format) { - uint32_t output_flags; + if (selected_format) { + struct format_info *info; - output_flags = obs_source_get_output_flags(obs_pw_stream->source); + da_init(obs_pw_stream->format_info); - if (output_flags & OBS_SOURCE_VIDEO) { - if (output_flags & OBS_SOURCE_ASYNC) - init_format_info_async(obs_pw_stream); - else - init_format_info_sync(obs_pw_stream); + info = da_push_back_new(obs_pw_stream->format_info); + da_init(info->modifiers); + info->spa_format = selected_format->spa_format; + info->drm_format = selected_format->drm_format; + } else { + uint32_t output_flags; + + output_flags = obs_source_get_output_flags(obs_pw_stream->source); + + if (output_flags & OBS_SOURCE_VIDEO) { + if (output_flags & OBS_SOURCE_ASYNC) + init_format_info_async(obs_pw_stream); + else + init_format_info_sync(obs_pw_stream); + } } } @@ -1072,7 +1083,7 @@ void obs_pipewire_destroy(obs_pipewire *obs_pw) } obs_pipewire_stream *obs_pipewire_connect_stream(obs_pipewire *obs_pw, obs_source_t *source, int pipewire_node, - const struct obs_pipwire_connect_stream_info *connect_info) + const struct obs_pipewire_connect_stream_info *connect_info) { struct spa_pod_builder pod_builder; const struct spa_pod **params = NULL; @@ -1095,7 +1106,7 @@ obs_pipewire_stream *obs_pipewire_connect_stream(obs_pipewire *obs_pw, obs_sourc if (obs_pw_stream->resolution.set) obs_pw_stream->resolution.rect = *connect_info->video.resolution; - init_format_info(obs_pw_stream); + init_format_info(obs_pw_stream, connect_info->video.format); pw_thread_loop_lock(obs_pw->thread_loop); diff --git a/plugins/linux-pipewire/pipewire.h b/plugins/linux-pipewire/pipewire.h index 1705ca696dd743..a4437f4bfce617 100644 --- a/plugins/linux-pipewire/pipewire.h +++ b/plugins/linux-pipewire/pipewire.h @@ -27,13 +27,15 @@ typedef struct _obs_pipewire obs_pipewire; typedef struct _obs_pipewire_stream obs_pipewire_stream; -struct obs_pipwire_connect_stream_info { +struct obs_pipewire_connect_stream_info { const char *stream_name; struct pw_properties *stream_properties; struct { bool cursor_visible; } screencast; struct { + const enum spa_media_subtype *subtype; + const struct obs_pw_video_format *format; const struct spa_rectangle *resolution; const struct spa_fraction *framerate; } video; @@ -46,7 +48,7 @@ void obs_pipewire_roundtrip(obs_pipewire *obs_pw); void obs_pipewire_destroy(obs_pipewire *obs_pw); obs_pipewire_stream *obs_pipewire_connect_stream(obs_pipewire *obs_pw, obs_source_t *source, int pipewire_node, - const struct obs_pipwire_connect_stream_info *connect_info); + const struct obs_pipewire_connect_stream_info *connect_info); void obs_pipewire_stream_show(obs_pipewire_stream *obs_pw_stream); void obs_pipewire_stream_hide(obs_pipewire_stream *obs_pw_stream); diff --git a/plugins/linux-pipewire/screencast-portal.c b/plugins/linux-pipewire/screencast-portal.c index 99c7c550624d71..d5fe531c218351 100644 --- a/plugins/linux-pipewire/screencast-portal.c +++ b/plugins/linux-pipewire/screencast-portal.c @@ -153,7 +153,7 @@ static const char *capture_type_to_string(enum obs_portal_capture_type capture_t static void on_pipewire_remote_opened_cb(GObject *source, GAsyncResult *res, void *user_data) { - struct obs_pipwire_connect_stream_info connect_info; + struct obs_pipewire_connect_stream_info connect_info; struct screencast_portal_capture *capture; g_autoptr(GUnixFDList) fd_list = NULL; g_autoptr(GVariant) result = NULL; @@ -183,7 +183,7 @@ static void on_pipewire_remote_opened_cb(GObject *source, GAsyncResult *res, voi if (!capture->obs_pw) return; - connect_info = (struct obs_pipwire_connect_stream_info){ + connect_info = (struct obs_pipewire_connect_stream_info){ .stream_name = "OBS Studio", .stream_properties = pw_properties_new(PW_KEY_MEDIA_TYPE, "Video", PW_KEY_MEDIA_CATEGORY, "Capture", PW_KEY_MEDIA_ROLE, "Screen", NULL),