Browse Source

linux-pipewire: Add resolution and framerate list contents

Trigger a renegotiation of the camera stream when either the
resolution or the framerate change.
Wim Taymans 3 năm trước cách đây
mục cha
commit
a44228ef50

+ 319 - 5
plugins/linux-pipewire/camera-portal.c

@@ -514,27 +514,318 @@ static bool device_selected(void *data, obs_properties_t *props,
 	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(properties);
 	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;
+	const struct spa_fraction *framerate_b = b;
+	double da = framerate_a->num / (double)framerate_a->denom;
+	double db = framerate_b->num / (double)framerate_b->denom;
+
+	return da - db;
+}
+
+static void framerate_list(struct camera_device *dev, uint32_t pixelformat,
+			   const struct spa_rectangle *resolution,
+			   obs_property_t *prop)
+{
+	g_autoptr(GHashTable) framerates_map = NULL;
+	g_autoptr(GArray) framerates = NULL;
+	struct param *p;
+	obs_data_t *data;
+
+	framerates = g_array_new(FALSE, FALSE, sizeof(struct spa_fraction));
+
+	spa_list_for_each(p, &dev->param_list, link)
+	{
+		const struct spa_fraction *framerate_values;
+		struct obs_pw_video_format obs_pw_video_format;
+		enum spa_choice_type choice;
+		const struct spa_pod_prop *prop;
+		struct spa_pod_parser pod_parser;
+		struct spa_rectangle this_resolution;
+		struct spa_fraction framerate;
+		struct spa_pod *framerate_pod;
+		uint32_t media_subtype;
+		uint32_t media_type;
+		uint32_t n_framerates;
+		uint32_t 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(&this_resolution)) < 0)
+			continue;
+
+		if (this_resolution.width != resolution->width ||
+		    this_resolution.height != resolution->height)
+			continue;
+
+		prop = spa_pod_find_prop(p->param, NULL,
+					 SPA_FORMAT_VIDEO_framerate);
+		if (!prop)
+			continue;
+
+		framerate_pod = spa_pod_get_values(&prop->value, &n_framerates,
+						   &choice);
+		if (framerate_pod->type != SPA_TYPE_Fraction) {
+			blog(LOG_WARNING,
+			     "Framerate is not a fraction - something is wrong");
+			continue;
+		}
+
+		framerate_values = SPA_POD_BODY(framerate_pod);
+
+		switch (choice) {
+		case SPA_CHOICE_None:
+			g_array_append_val(framerates, framerate_values[0]);
+			break;
+		case SPA_CHOICE_Range:
+			blog(LOG_WARNING, "Ranged framerates not supported");
+			break;
+		case SPA_CHOICE_Step:
+			blog(LOG_WARNING, "Stepped framerates not supported");
+			break;
+		case SPA_CHOICE_Enum:
+			/* i=0 is the default framerate, skip it */
+			for (uint32_t i = 1; i < n_framerates; i++)
+				g_array_append_val(framerates,
+						   framerate_values[i]);
+			break;
+		default:
+			break;
+		}
+	}
+
+	g_array_sort(framerates, compare_framerates);
+
+	obs_property_list_clear(prop);
+
+	data = obs_data_create();
+	for (size_t i = 0; i < framerates->len; i++) {
+		const struct spa_fraction *framerate =
+			&g_array_index(framerates, struct spa_fraction, i);
+		struct media_frames_per_second fps;
+		struct dstr str = {};
+
+		fps = (struct media_frames_per_second){
+			.numerator = framerate->num,
+			.denominator = framerate->denom,
+		};
+		obs_data_set_frames_per_second(data, "framerate", fps, NULL);
+
+		dstr_printf(&str, "%.2f",
+			    framerate->num / (double)framerate->denom);
+		obs_property_list_add_string(prop, str.array,
+					     obs_data_get_json(data));
+
+		dstr_free(&str);
+	}
+	obs_data_release(data);
+}
+
+static bool parse_framerate(struct spa_fraction *dest, const char *json)
+{
+	struct media_frames_per_second fps;
+	obs_data_t *data = obs_data_create_from_json(json);
+
+	if (!data)
+		return false;
+
+	if (!obs_data_get_frames_per_second(data, "framerate", &fps, NULL)) {
+		obs_data_release(data);
+		return false;
+	}
+
+	dest->num = fps.numerator;
+	dest->denom = fps.denominator;
+
+	obs_data_release(data);
+	return true;
+}
+
+static bool framerate_selected(void *data, obs_properties_t *properties,
+			       obs_property_t *property, obs_data_t *settings)
+{
+	UNUSED_PARAMETER(properties);
+	UNUSED_PARAMETER(property);
+
+	struct camera_portal_source *camera_source = data;
+	struct camera_device *device;
+	struct spa_fraction framerate;
+
+	device = g_hash_table_lookup(connection->devices,
+				     camera_source->device_id);
+	if (device == NULL)
+		return false;
+
+	if (!parse_framerate(&framerate,
+			     obs_data_get_string(settings, "framerate")))
+		return false;
+
+	if (camera_source->obs_pw_stream)
+		obs_pipewire_stream_set_framerate(camera_source->obs_pw_stream,
+						  &framerate);
+
 	return true;
 }
 
 /*
  * Resolution selected callback
  */
+
+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 bool resolution_selected(void *data, obs_properties_t *properties,
 				obs_property_t *property, obs_data_t *settings)
 {
@@ -543,10 +834,29 @@ static bool resolution_selected(void *data, obs_properties_t *properties,
 	UNUSED_PARAMETER(settings);
 
 	struct camera_portal_source *camera_source = data;
+	struct spa_rectangle resolution;
+	struct camera_device *device;
 
 	blog(LOG_INFO, "[camera-portal] Selected resolution 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")))
+		return false;
+
+	if (camera_source->obs_pw_stream)
+		obs_pipewire_stream_set_resolution(camera_source->obs_pw_stream,
+						   &resolution);
+
+	property = obs_properties_get(properties, "framerate");
+	framerate_list(device, obs_data_get_int(settings, "pixelformat"),
+		       &resolution, property);
+
 	return true;
 }
 
@@ -931,6 +1241,7 @@ static obs_properties_t *pipewire_camera_get_properties(void *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;
 
@@ -948,11 +1259,12 @@ static obs_properties_t *pipewire_camera_get_properties(void *data)
 	resolution_list = obs_properties_add_list(props, "resolution",
 						  obs_module_text("Resolution"),
 						  OBS_COMBO_TYPE_LIST,
-						  OBS_COMBO_FORMAT_INT);
+						  OBS_COMBO_FORMAT_STRING);
 
-	obs_properties_add_list(props, "framerate",
-				obs_module_text("FrameRate"),
-				OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);
+	framerate_list = obs_properties_add_list(props, "framerate",
+						 obs_module_text("FrameRate"),
+						 OBS_COMBO_TYPE_LIST,
+						 OBS_COMBO_FORMAT_STRING);
 
 	// a group to contain the camera control
 	controls_props = obs_properties_create();
@@ -968,6 +1280,8 @@ static obs_properties_t *pipewire_camera_get_properties(void *data)
 					    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;
 }

+ 101 - 14
plugins/linux-pipewire/pipewire.c

@@ -128,6 +128,16 @@ struct _obs_pipewire_stream {
 	bool negotiated;
 
 	DARRAY(struct format_info) format_info;
+
+	struct {
+		struct spa_rectangle rect;
+		bool set;
+	} resolution;
+
+	struct {
+		struct spa_fraction fraction;
+		bool set;
+	} framerate;
 };
 
 /* auxiliary methods */
@@ -288,12 +298,41 @@ static void swap_texture_red_blue(gs_texture_t *texture)
 	glBindTexture(GL_TEXTURE_2D, 0);
 }
 
-static inline struct spa_pod *build_format(struct spa_pod_builder *b,
-					   struct obs_video_info *ovi,
+static inline struct spa_pod *build_format(obs_pipewire_stream *obs_pw_stream,
+					   struct spa_pod_builder *b,
 					   uint32_t format, uint64_t *modifiers,
 					   size_t modifier_count)
 {
+	struct spa_rectangle max_resolution = SPA_RECTANGLE(8192, 4320);
+	struct spa_rectangle min_resolution = SPA_RECTANGLE(1, 1);
+	struct spa_rectangle resolution;
 	struct spa_pod_frame format_frame;
+	struct spa_fraction max_framerate;
+	struct spa_fraction min_framerate;
+	struct spa_fraction framerate;
+
+	if (obs_pw_stream->framerate.set) {
+		framerate = obs_pw_stream->framerate.fraction;
+		min_framerate = obs_pw_stream->framerate.fraction;
+		max_framerate = obs_pw_stream->framerate.fraction;
+	} else {
+		framerate = SPA_FRACTION(obs_pw_stream->video_info.fps_num,
+					 obs_pw_stream->video_info.fps_den);
+		min_framerate = SPA_FRACTION(0, 1);
+		max_framerate = SPA_FRACTION(360, 1);
+	}
+
+	if (obs_pw_stream->resolution.set) {
+		resolution = obs_pw_stream->resolution.rect;
+		min_resolution = obs_pw_stream->resolution.rect;
+		max_resolution = obs_pw_stream->resolution.rect;
+	} else {
+		resolution =
+			SPA_RECTANGLE(obs_pw_stream->video_info.output_width,
+				      obs_pw_stream->video_info.output_height);
+		min_resolution = SPA_RECTANGLE(1, 1);
+		max_resolution = SPA_RECTANGLE(8192, 4320);
+	}
 
 	/* Make an object of type SPA_TYPE_OBJECT_Format and id SPA_PARAM_EnumFormat.
 	 * The object type is important because it defines the properties that are
@@ -334,16 +373,14 @@ static inline struct spa_pod *build_format(struct spa_pod_builder *b,
 		spa_pod_builder_pop(b, &modifier_frame);
 	}
 	/* add size and framerate ranges */
-	spa_pod_builder_add(b, SPA_FORMAT_VIDEO_size,
-			    SPA_POD_CHOICE_RANGE_Rectangle(
-				    &SPA_RECTANGLE(320, 240), // Arbitrary
-				    &SPA_RECTANGLE(1, 1),
-				    &SPA_RECTANGLE(8192, 4320)),
-			    SPA_FORMAT_VIDEO_framerate,
-			    SPA_POD_CHOICE_RANGE_Fraction(
-				    &SPA_FRACTION(ovi->fps_num, ovi->fps_den),
-				    &SPA_FRACTION(0, 1), &SPA_FRACTION(360, 1)),
-			    0);
+	spa_pod_builder_add(
+		b, SPA_FORMAT_VIDEO_size,
+		SPA_POD_CHOICE_RANGE_Rectangle(&resolution, &min_resolution,
+					       &max_resolution),
+		SPA_FORMAT_VIDEO_framerate,
+		SPA_POD_CHOICE_RANGE_Fraction(&framerate, &min_framerate,
+					      &max_framerate),
+		0);
 	return spa_pod_builder_pop(b, &format_frame);
 }
 
@@ -373,7 +410,7 @@ static bool build_format_params(obs_pipewire_stream *obs_pw_stream,
 			continue;
 		}
 		params[params_count++] = build_format(
-			pod_builder, &obs_pw_stream->video_info,
+			obs_pw_stream, pod_builder,
 			obs_pw_stream->format_info.array[i].spa_format,
 			obs_pw_stream->format_info.array[i].modifiers.array,
 			obs_pw_stream->format_info.array[i].modifiers.num);
@@ -382,7 +419,7 @@ static bool build_format_params(obs_pipewire_stream *obs_pw_stream,
 build_shm:
 	for (size_t i = 0; i < obs_pw_stream->format_info.num; i++) {
 		params[params_count++] = build_format(
-			pod_builder, &obs_pw_stream->video_info,
+			obs_pw_stream, pod_builder,
 			obs_pw_stream->format_info.array[i].spa_format, NULL,
 			0);
 	}
@@ -1178,6 +1215,8 @@ obs_pipewire_stream *obs_pipewire_connect_stream(
 	obs_pw_stream->obs_pw = obs_pw;
 	obs_pw_stream->source = source;
 	obs_pw_stream->cursor.visible = connect_info->screencast.cursor_visible;
+	obs_pw_stream->framerate.set = false;
+	obs_pw_stream->resolution.set = false;
 
 	init_format_info(obs_pw_stream);
 
@@ -1390,3 +1429,51 @@ void obs_pipewire_stream_destroy(obs_pipewire_stream *obs_pw_stream)
 	clear_format_info(obs_pw_stream);
 	bfree(obs_pw_stream);
 }
+
+void obs_pipewire_stream_set_framerate(obs_pipewire_stream *obs_pw_stream,
+				       const struct spa_fraction *framerate)
+{
+	obs_pipewire *obs_pw = obs_pw_stream->obs_pw;
+
+	if ((!obs_pw_stream->framerate.set && !framerate) ||
+	    (obs_pw_stream->framerate.set && framerate &&
+	     obs_pw_stream->framerate.fraction.num == framerate->num &&
+	     obs_pw_stream->framerate.fraction.denom == framerate->denom))
+		return;
+
+	if (framerate) {
+		obs_pw_stream->framerate.fraction = *framerate;
+		obs_pw_stream->framerate.set = true;
+	} else {
+		obs_pw_stream->framerate.fraction = SPA_FRACTION(0, 0);
+		obs_pw_stream->framerate.set = false;
+	}
+
+	/* Signal to renegotiate */
+	pw_loop_signal_event(pw_thread_loop_get_loop(obs_pw->thread_loop),
+			     obs_pw_stream->reneg);
+}
+
+void obs_pipewire_stream_set_resolution(obs_pipewire_stream *obs_pw_stream,
+					const struct spa_rectangle *resolution)
+{
+	obs_pipewire *obs_pw = obs_pw_stream->obs_pw;
+
+	if ((!obs_pw_stream->resolution.set && !resolution) ||
+	    (obs_pw_stream->resolution.set && resolution &&
+	     obs_pw_stream->resolution.rect.width == resolution->width &&
+	     obs_pw_stream->resolution.rect.height == resolution->height))
+		return;
+
+	if (resolution) {
+		obs_pw_stream->resolution.rect = *resolution;
+		obs_pw_stream->resolution.set = true;
+	} else {
+		obs_pw_stream->resolution.rect = SPA_RECTANGLE(0, 0);
+		obs_pw_stream->resolution.set = false;
+	}
+
+	/* Signal to renegotiate */
+	pw_loop_signal_event(pw_thread_loop_get_loop(obs_pw->thread_loop),
+			     obs_pw_stream->reneg);
+}

+ 5 - 0
plugins/linux-pipewire/pipewire.h

@@ -57,3 +57,8 @@ void obs_pipewire_stream_video_render(obs_pipewire_stream *obs_pw_stream,
 void obs_pipewire_stream_set_cursor_visible(obs_pipewire_stream *obs_pw_stream,
 					    bool cursor_visible);
 void obs_pipewire_stream_destroy(obs_pipewire_stream *obs_pw_stream);
+
+void obs_pipewire_stream_set_framerate(obs_pipewire_stream *obs_pw_stream,
+				       const struct spa_fraction *framerate);
+void obs_pipewire_stream_set_resolution(obs_pipewire_stream *obs_pw,
+					const struct spa_rectangle *resolution);