mirror of
https://github.com/brl/mutter.git
synced 2024-12-22 19:12:04 +00:00
screen-cast: Add 'cursor-mode' to allow decoupled cursor updates
The 'cursor-mode', which currently is limited to RecordMonitor(), allows the user to either do screen casts where the cursor is hidden, embedded in the framebuffer, or sent as PipeWire stream metadata. The latter allows the user to get cursor updates sent, including the cursor sprite, without requiring a stage paint each frame. Currently this is done by using the cursor sprite texture, and either reading directly from, or drawing to an offscreen framebuffer which is read from instead, in case the texture is scaled. https://gitlab.gnome.org/GNOME/mutter/merge_requests/357
This commit is contained in:
parent
79d99cbe3f
commit
4e402b3972
@ -13,5 +13,8 @@ RUN dnf -y update && dnf -y upgrade && \
|
||||
# Unpackaged versions
|
||||
dnf install -y https://copr-be.cloud.fedoraproject.org/results/jadahl/mutter-ci/fedora-29-x86_64/00836095-gsettings-desktop-schemas/gsettings-desktop-schemas-3.30.1-1.20181206git918efdd69be53.fc29.x86_64.rpm https://copr-be.cloud.fedoraproject.org/results/jadahl/mutter-ci/fedora-29-x86_64/00836095-gsettings-desktop-schemas/gsettings-desktop-schemas-devel-3.30.1-1.20181206git918efdd69be53.fc29.x86_64.rpm && \
|
||||
|
||||
# Packages not yet in stable
|
||||
dnf install -y https://kojipkgs.fedoraproject.org//packages/pipewire/0.2.5/1.fc29/x86_64/pipewire-0.2.5-1.fc29.x86_64.rpm https://kojipkgs.fedoraproject.org//packages/pipewire/0.2.5/1.fc29/x86_64/pipewire-devel-0.2.5-1.fc29.x86_64.rpm https://kojipkgs.fedoraproject.org//packages/pipewire/0.2.5/1.fc29/x86_64/pipewire-libs-0.2.5-1.fc29.x86_64.rpm && \
|
||||
|
||||
dnf install -y intltool redhat-rpm-config make && \
|
||||
dnf clean all
|
||||
|
@ -43,7 +43,7 @@ libinput_req = '>= 1.4'
|
||||
gbm_req = '>= 10.3'
|
||||
|
||||
# screen cast version requirements
|
||||
libpipewire_req = '>= 0.2.2'
|
||||
libpipewire_req = '>= 0.2.5'
|
||||
|
||||
gnome = import('gnome')
|
||||
pkg = import('pkgconfig')
|
||||
|
@ -24,23 +24,36 @@
|
||||
|
||||
#include "backends/meta-screen-cast-monitor-stream-src.h"
|
||||
|
||||
#include <spa/buffer/meta.h>
|
||||
|
||||
#include "backends/meta-backend-private.h"
|
||||
#include "backends/meta-cursor-tracker-private.h"
|
||||
#include "backends/meta-logical-monitor.h"
|
||||
#include "backends/meta-monitor.h"
|
||||
#include "backends/meta-screen-cast-monitor-stream.h"
|
||||
#include "backends/meta-screen-cast-session.h"
|
||||
#include "clutter/clutter.h"
|
||||
#include "clutter/clutter-mutter.h"
|
||||
#include "core/boxes-private.h"
|
||||
|
||||
struct _MetaScreenCastMonitorStreamSrc
|
||||
{
|
||||
MetaScreenCastStreamSrc parent;
|
||||
|
||||
gulong actors_painted_handler_id;
|
||||
gulong paint_handler_id;
|
||||
gulong cursor_moved_handler_id;
|
||||
gulong cursor_changed_handler_id;
|
||||
};
|
||||
|
||||
G_DEFINE_TYPE (MetaScreenCastMonitorStreamSrc,
|
||||
meta_screen_cast_monitor_stream_src,
|
||||
META_TYPE_SCREEN_CAST_STREAM_SRC)
|
||||
static void
|
||||
hw_cursor_inhibitor_iface_init (MetaHwCursorInhibitorInterface *iface);
|
||||
|
||||
G_DEFINE_TYPE_WITH_CODE (MetaScreenCastMonitorStreamSrc,
|
||||
meta_screen_cast_monitor_stream_src,
|
||||
META_TYPE_SCREEN_CAST_STREAM_SRC,
|
||||
G_IMPLEMENT_INTERFACE (META_TYPE_HW_CURSOR_INHIBITOR,
|
||||
hw_cursor_inhibitor_iface_init))
|
||||
|
||||
static ClutterStage *
|
||||
get_stage (MetaScreenCastMonitorStreamSrc *monitor_src)
|
||||
@ -102,18 +115,163 @@ stage_painted (ClutterActor *actor,
|
||||
meta_screen_cast_stream_src_maybe_record_frame (src);
|
||||
}
|
||||
|
||||
static MetaBackend *
|
||||
get_backend (MetaScreenCastMonitorStreamSrc *monitor_src)
|
||||
{
|
||||
MetaScreenCastStreamSrc *src = META_SCREEN_CAST_STREAM_SRC (monitor_src);
|
||||
MetaScreenCastStream *stream = meta_screen_cast_stream_src_get_stream (src);
|
||||
MetaScreenCastSession *session = meta_screen_cast_stream_get_session (stream);
|
||||
MetaScreenCast *screen_cast =
|
||||
meta_screen_cast_session_get_screen_cast (session);
|
||||
|
||||
return meta_screen_cast_get_backend (screen_cast);
|
||||
}
|
||||
|
||||
static gboolean
|
||||
is_cursor_in_stream (MetaScreenCastMonitorStreamSrc *monitor_src)
|
||||
{
|
||||
MetaBackend *backend = get_backend (monitor_src);
|
||||
MetaCursorRenderer *cursor_renderer =
|
||||
meta_backend_get_cursor_renderer (backend);
|
||||
MetaMonitor *monitor;
|
||||
MetaLogicalMonitor *logical_monitor;
|
||||
MetaRectangle logical_monitor_layout;
|
||||
ClutterRect logical_monitor_rect;
|
||||
MetaCursorSprite *cursor_sprite;
|
||||
|
||||
monitor = get_monitor (monitor_src);
|
||||
logical_monitor = meta_monitor_get_logical_monitor (monitor);
|
||||
logical_monitor_layout = meta_logical_monitor_get_layout (logical_monitor);
|
||||
logical_monitor_rect =
|
||||
meta_rectangle_to_clutter_rect (&logical_monitor_layout);
|
||||
|
||||
cursor_sprite = meta_cursor_renderer_get_cursor (cursor_renderer);
|
||||
if (cursor_sprite)
|
||||
{
|
||||
ClutterRect cursor_rect;
|
||||
|
||||
cursor_rect = meta_cursor_renderer_calculate_rect (cursor_renderer,
|
||||
cursor_sprite);
|
||||
return clutter_rect_intersection (&cursor_rect,
|
||||
&logical_monitor_rect,
|
||||
NULL);
|
||||
}
|
||||
else
|
||||
{
|
||||
ClutterPoint cursor_position;
|
||||
|
||||
cursor_position = meta_cursor_renderer_get_position (cursor_renderer);
|
||||
return clutter_rect_contains_point (&logical_monitor_rect,
|
||||
&cursor_position);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
sync_cursor_state (MetaScreenCastMonitorStreamSrc *monitor_src)
|
||||
{
|
||||
MetaScreenCastStreamSrc *src = META_SCREEN_CAST_STREAM_SRC (monitor_src);
|
||||
ClutterStage *stage = get_stage (monitor_src);
|
||||
|
||||
if (!is_cursor_in_stream (monitor_src))
|
||||
return;
|
||||
|
||||
if (clutter_stage_is_redraw_queued (stage))
|
||||
return;
|
||||
|
||||
meta_screen_cast_stream_src_maybe_record_frame (src);
|
||||
}
|
||||
|
||||
static void
|
||||
cursor_moved (MetaCursorTracker *cursor_tracker,
|
||||
float x,
|
||||
float y,
|
||||
MetaScreenCastMonitorStreamSrc *monitor_src)
|
||||
{
|
||||
sync_cursor_state (monitor_src);
|
||||
}
|
||||
|
||||
static void
|
||||
cursor_changed (MetaCursorTracker *cursor_tracker,
|
||||
MetaScreenCastMonitorStreamSrc *monitor_src)
|
||||
{
|
||||
sync_cursor_state (monitor_src);
|
||||
}
|
||||
|
||||
static MetaCursorRenderer *
|
||||
get_cursor_renderer (MetaScreenCastMonitorStreamSrc *monitor_src)
|
||||
{
|
||||
MetaScreenCastStreamSrc *src = META_SCREEN_CAST_STREAM_SRC (monitor_src);
|
||||
MetaScreenCastStream *stream = meta_screen_cast_stream_src_get_stream (src);
|
||||
MetaScreenCastSession *session = meta_screen_cast_stream_get_session (stream);
|
||||
MetaScreenCast *screen_cast =
|
||||
meta_screen_cast_session_get_screen_cast (session);
|
||||
MetaBackend *backend = meta_screen_cast_get_backend (screen_cast);
|
||||
|
||||
return meta_backend_get_cursor_renderer (backend);
|
||||
}
|
||||
|
||||
static void
|
||||
inhibit_hw_cursor (MetaScreenCastMonitorStreamSrc *monitor_src)
|
||||
{
|
||||
MetaCursorRenderer *cursor_renderer;
|
||||
MetaHwCursorInhibitor *inhibitor;
|
||||
|
||||
cursor_renderer = get_cursor_renderer (monitor_src);
|
||||
inhibitor = META_HW_CURSOR_INHIBITOR (monitor_src);
|
||||
meta_cursor_renderer_add_hw_cursor_inhibitor (cursor_renderer, inhibitor);
|
||||
}
|
||||
|
||||
static void
|
||||
uninhibit_hw_cursor (MetaScreenCastMonitorStreamSrc *monitor_src)
|
||||
{
|
||||
MetaCursorRenderer *cursor_renderer;
|
||||
MetaHwCursorInhibitor *inhibitor;
|
||||
|
||||
cursor_renderer = get_cursor_renderer (monitor_src);
|
||||
inhibitor = META_HW_CURSOR_INHIBITOR (monitor_src);
|
||||
meta_cursor_renderer_remove_hw_cursor_inhibitor (cursor_renderer, inhibitor);
|
||||
}
|
||||
|
||||
static void
|
||||
meta_screen_cast_monitor_stream_src_enable (MetaScreenCastStreamSrc *src)
|
||||
{
|
||||
MetaScreenCastMonitorStreamSrc *monitor_src =
|
||||
META_SCREEN_CAST_MONITOR_STREAM_SRC (src);
|
||||
MetaBackend *backend = get_backend (monitor_src);
|
||||
MetaCursorTracker *cursor_tracker = meta_backend_get_cursor_tracker (backend);
|
||||
ClutterStage *stage;
|
||||
MetaScreenCastStream *stream;
|
||||
|
||||
stream = meta_screen_cast_stream_src_get_stream (src);
|
||||
stage = get_stage (monitor_src);
|
||||
monitor_src->actors_painted_handler_id =
|
||||
g_signal_connect_after (stage, "actors-painted",
|
||||
G_CALLBACK (stage_painted),
|
||||
monitor_src);
|
||||
|
||||
switch (meta_screen_cast_stream_get_cursor_mode (stream))
|
||||
{
|
||||
case META_SCREEN_CAST_CURSOR_MODE_METADATA:
|
||||
monitor_src->cursor_moved_handler_id =
|
||||
g_signal_connect_after (cursor_tracker, "cursor-moved",
|
||||
G_CALLBACK (cursor_moved),
|
||||
monitor_src);
|
||||
monitor_src->cursor_changed_handler_id =
|
||||
g_signal_connect_after (cursor_tracker, "cursor-changed",
|
||||
G_CALLBACK (cursor_changed),
|
||||
monitor_src);
|
||||
/* Intentional fall-through */
|
||||
case META_SCREEN_CAST_CURSOR_MODE_HIDDEN:
|
||||
monitor_src->actors_painted_handler_id =
|
||||
g_signal_connect (stage, "actors-painted",
|
||||
G_CALLBACK (stage_painted),
|
||||
monitor_src);
|
||||
break;
|
||||
case META_SCREEN_CAST_CURSOR_MODE_EMBEDDED:
|
||||
inhibit_hw_cursor (monitor_src);
|
||||
monitor_src->paint_handler_id =
|
||||
g_signal_connect_after (stage, "paint",
|
||||
G_CALLBACK (stage_painted),
|
||||
monitor_src);
|
||||
break;
|
||||
}
|
||||
|
||||
clutter_actor_queue_redraw (CLUTTER_ACTOR (stage));
|
||||
}
|
||||
|
||||
@ -122,14 +280,43 @@ meta_screen_cast_monitor_stream_src_disable (MetaScreenCastStreamSrc *src)
|
||||
{
|
||||
MetaScreenCastMonitorStreamSrc *monitor_src =
|
||||
META_SCREEN_CAST_MONITOR_STREAM_SRC (src);
|
||||
MetaBackend *backend = get_backend (monitor_src);
|
||||
MetaCursorTracker *cursor_tracker = meta_backend_get_cursor_tracker (backend);
|
||||
ClutterStage *stage;
|
||||
|
||||
stage = get_stage (monitor_src);
|
||||
g_signal_handler_disconnect (stage, monitor_src->actors_painted_handler_id);
|
||||
monitor_src->actors_painted_handler_id = 0;
|
||||
|
||||
if (monitor_src->actors_painted_handler_id)
|
||||
{
|
||||
g_signal_handler_disconnect (stage,
|
||||
monitor_src->actors_painted_handler_id);
|
||||
monitor_src->actors_painted_handler_id = 0;
|
||||
}
|
||||
|
||||
if (monitor_src->paint_handler_id)
|
||||
{
|
||||
g_signal_handler_disconnect (stage,
|
||||
monitor_src->paint_handler_id);
|
||||
monitor_src->paint_handler_id = 0;
|
||||
uninhibit_hw_cursor (monitor_src);
|
||||
}
|
||||
|
||||
if (monitor_src->cursor_moved_handler_id)
|
||||
{
|
||||
g_signal_handler_disconnect (cursor_tracker,
|
||||
monitor_src->cursor_moved_handler_id);
|
||||
monitor_src->cursor_moved_handler_id = 0;
|
||||
}
|
||||
|
||||
if (monitor_src->cursor_changed_handler_id)
|
||||
{
|
||||
g_signal_handler_disconnect (cursor_tracker,
|
||||
monitor_src->cursor_changed_handler_id);
|
||||
monitor_src->cursor_changed_handler_id = 0;
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
static gboolean
|
||||
meta_screen_cast_monitor_stream_src_record_frame (MetaScreenCastStreamSrc *src,
|
||||
uint8_t *data)
|
||||
{
|
||||
@ -140,9 +327,216 @@ meta_screen_cast_monitor_stream_src_record_frame (MetaScreenCastStreamSrc *src,
|
||||
MetaLogicalMonitor *logical_monitor;
|
||||
|
||||
stage = get_stage (monitor_src);
|
||||
if (!clutter_stage_is_redraw_queued (stage))
|
||||
return FALSE;
|
||||
|
||||
monitor = get_monitor (monitor_src);
|
||||
logical_monitor = meta_monitor_get_logical_monitor (monitor);
|
||||
clutter_stage_capture_into (stage, FALSE, &logical_monitor->rect, data);
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
draw_cursor_sprite_via_offscreen (MetaScreenCastMonitorStreamSrc *monitor_src,
|
||||
CoglTexture *cursor_texture,
|
||||
int bitmap_width,
|
||||
int bitmap_height,
|
||||
uint32_t *bitmap_data,
|
||||
GError **error)
|
||||
{
|
||||
MetaBackend *backend = get_backend (monitor_src);
|
||||
ClutterBackend *clutter_backend = meta_backend_get_clutter_backend (backend);
|
||||
CoglContext *cogl_context =
|
||||
clutter_backend_get_cogl_context (clutter_backend);
|
||||
CoglTexture2D *bitmap_texture;
|
||||
CoglOffscreen *offscreen;
|
||||
CoglFramebuffer *fb;
|
||||
CoglPipeline *pipeline;
|
||||
CoglColor clear_color;
|
||||
|
||||
bitmap_texture = cogl_texture_2d_new_with_size (cogl_context,
|
||||
bitmap_width, bitmap_height);
|
||||
cogl_primitive_texture_set_auto_mipmap (COGL_PRIMITIVE_TEXTURE (bitmap_texture),
|
||||
FALSE);
|
||||
if (!cogl_texture_allocate (COGL_TEXTURE (bitmap_texture), error))
|
||||
{
|
||||
cogl_object_unref (bitmap_texture);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
offscreen = cogl_offscreen_new_with_texture (COGL_TEXTURE (bitmap_texture));
|
||||
fb = COGL_FRAMEBUFFER (offscreen);
|
||||
cogl_object_unref (bitmap_texture);
|
||||
if (!cogl_framebuffer_allocate (fb, error))
|
||||
{
|
||||
cogl_object_unref (fb);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
pipeline = cogl_pipeline_new (cogl_context);
|
||||
cogl_pipeline_set_layer_texture (pipeline, 0, cursor_texture);
|
||||
cogl_pipeline_set_layer_filters (pipeline, 0,
|
||||
COGL_PIPELINE_FILTER_LINEAR,
|
||||
COGL_PIPELINE_FILTER_LINEAR);
|
||||
cogl_color_init_from_4ub (&clear_color, 0, 0, 0, 0);
|
||||
cogl_framebuffer_clear (fb, COGL_BUFFER_BIT_COLOR, &clear_color);
|
||||
cogl_framebuffer_draw_rectangle (fb, pipeline,
|
||||
-1, 1, 1, -1);
|
||||
cogl_object_unref (pipeline);
|
||||
|
||||
cogl_framebuffer_read_pixels (fb,
|
||||
0, 0,
|
||||
bitmap_width, bitmap_height,
|
||||
COGL_PIXEL_FORMAT_RGBA_8888_PRE,
|
||||
(uint8_t *) bitmap_data);
|
||||
cogl_object_unref (fb);
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static void
|
||||
meta_screen_cast_monitor_stream_src_set_cursor_metadata (MetaScreenCastStreamSrc *src,
|
||||
struct spa_meta_cursor *spa_meta_cursor)
|
||||
{
|
||||
MetaScreenCastMonitorStreamSrc *monitor_src =
|
||||
META_SCREEN_CAST_MONITOR_STREAM_SRC (src);
|
||||
MetaBackend *backend = get_backend (monitor_src);
|
||||
MetaCursorRenderer *cursor_renderer =
|
||||
meta_backend_get_cursor_renderer (backend);
|
||||
MetaRenderer *renderer = meta_backend_get_renderer (backend);
|
||||
MetaSpaType *spa_type = meta_screen_cast_stream_src_get_spa_type (src);
|
||||
GError *error = NULL;
|
||||
MetaCursorSprite *cursor_sprite;
|
||||
CoglTexture *cursor_texture;
|
||||
MetaMonitor *monitor;
|
||||
MetaLogicalMonitor *logical_monitor;
|
||||
MetaRectangle logical_monitor_layout;
|
||||
ClutterRect logical_monitor_rect;
|
||||
MetaRendererView *view;
|
||||
float view_scale;
|
||||
ClutterPoint cursor_position;
|
||||
struct spa_meta_bitmap *spa_meta_bitmap;
|
||||
|
||||
cursor_sprite = meta_cursor_renderer_get_cursor (cursor_renderer);
|
||||
if (cursor_sprite)
|
||||
cursor_texture = meta_cursor_sprite_get_cogl_texture (cursor_sprite);
|
||||
else
|
||||
cursor_texture = NULL;
|
||||
|
||||
if (!is_cursor_in_stream (monitor_src))
|
||||
{
|
||||
spa_meta_cursor->id = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
monitor = get_monitor (monitor_src);
|
||||
logical_monitor = meta_monitor_get_logical_monitor (monitor);
|
||||
logical_monitor_layout = meta_logical_monitor_get_layout (logical_monitor);
|
||||
logical_monitor_rect =
|
||||
meta_rectangle_to_clutter_rect (&logical_monitor_layout);
|
||||
|
||||
view = meta_renderer_get_view_from_logical_monitor (renderer,
|
||||
logical_monitor);
|
||||
if (view)
|
||||
view_scale = clutter_stage_view_get_scale (CLUTTER_STAGE_VIEW (view));
|
||||
else
|
||||
view_scale = 1.0;
|
||||
|
||||
cursor_position = meta_cursor_renderer_get_position (cursor_renderer);
|
||||
cursor_position.x -= logical_monitor_rect.origin.x;
|
||||
cursor_position.y -= logical_monitor_rect.origin.y;
|
||||
cursor_position.x *= view_scale;
|
||||
cursor_position.y *= view_scale;
|
||||
|
||||
spa_meta_cursor->id = 1;
|
||||
spa_meta_cursor->position.x = (int32_t) roundf (cursor_position.x);
|
||||
spa_meta_cursor->position.y = (int32_t) roundf (cursor_position.y);
|
||||
spa_meta_cursor->bitmap_offset = sizeof (struct spa_meta_cursor);
|
||||
|
||||
spa_meta_bitmap = SPA_MEMBER (spa_meta_cursor,
|
||||
spa_meta_cursor->bitmap_offset,
|
||||
struct spa_meta_bitmap);
|
||||
spa_meta_bitmap->format = spa_type->video_format.RGBA;
|
||||
spa_meta_bitmap->offset = sizeof (struct spa_meta_bitmap);
|
||||
|
||||
if (cursor_texture)
|
||||
{
|
||||
float cursor_scale;
|
||||
float bitmap_scale;
|
||||
int hotspot_x, hotspot_y;
|
||||
int texture_width, texture_height;
|
||||
int bitmap_width, bitmap_height;
|
||||
uint32_t *bitmap_data;
|
||||
|
||||
cursor_scale = meta_cursor_sprite_get_texture_scale (cursor_sprite);
|
||||
bitmap_scale = view_scale * cursor_scale;
|
||||
|
||||
meta_cursor_sprite_get_hotspot (cursor_sprite, &hotspot_x, &hotspot_y);
|
||||
spa_meta_cursor->hotspot.x = (int32_t) roundf (hotspot_x * bitmap_scale);
|
||||
spa_meta_cursor->hotspot.y = (int32_t) roundf (hotspot_y * bitmap_scale);
|
||||
|
||||
texture_width = cogl_texture_get_width (cursor_texture);
|
||||
texture_height = cogl_texture_get_height (cursor_texture);
|
||||
bitmap_width = texture_width * bitmap_scale;
|
||||
bitmap_height = texture_height * bitmap_scale;
|
||||
|
||||
spa_meta_bitmap->size.width = bitmap_width;
|
||||
spa_meta_bitmap->size.height = bitmap_height;
|
||||
spa_meta_bitmap->stride = bitmap_width * 4;
|
||||
|
||||
bitmap_data = SPA_MEMBER (spa_meta_bitmap,
|
||||
spa_meta_bitmap->offset,
|
||||
uint32_t);
|
||||
|
||||
if (texture_width == bitmap_width &&
|
||||
texture_height == bitmap_height)
|
||||
{
|
||||
cogl_texture_get_data (cursor_texture,
|
||||
COGL_PIXEL_FORMAT_RGBA_8888_PRE,
|
||||
texture_width * 4,
|
||||
(uint8_t *) bitmap_data);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!draw_cursor_sprite_via_offscreen (monitor_src,
|
||||
cursor_texture,
|
||||
bitmap_width,
|
||||
bitmap_height,
|
||||
bitmap_data,
|
||||
&error))
|
||||
{
|
||||
g_warning ("Failed to draw cursor via offscreen: %s",
|
||||
error->message);
|
||||
g_error_free (error);
|
||||
spa_meta_cursor->id = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
spa_meta_cursor->hotspot.x = 0;
|
||||
spa_meta_cursor->hotspot.y = 0;
|
||||
|
||||
*spa_meta_bitmap = (struct spa_meta_bitmap) { 0 };
|
||||
}
|
||||
}
|
||||
|
||||
static gboolean
|
||||
meta_screen_cast_monitor_stream_src_is_cursor_sprite_inhibited (MetaHwCursorInhibitor *inhibitor,
|
||||
MetaCursorSprite *cursor_sprite)
|
||||
{
|
||||
MetaScreenCastMonitorStreamSrc *monitor_src =
|
||||
META_SCREEN_CAST_MONITOR_STREAM_SRC (inhibitor);
|
||||
|
||||
return is_cursor_in_stream (monitor_src);
|
||||
}
|
||||
|
||||
static void
|
||||
hw_cursor_inhibitor_iface_init (MetaHwCursorInhibitorInterface *iface)
|
||||
{
|
||||
iface->is_cursor_sprite_inhibited =
|
||||
meta_screen_cast_monitor_stream_src_is_cursor_sprite_inhibited;
|
||||
}
|
||||
|
||||
MetaScreenCastMonitorStreamSrc *
|
||||
@ -169,4 +563,6 @@ meta_screen_cast_monitor_stream_src_class_init (MetaScreenCastMonitorStreamSrcCl
|
||||
src_class->enable = meta_screen_cast_monitor_stream_src_enable;
|
||||
src_class->disable = meta_screen_cast_monitor_stream_src_disable;
|
||||
src_class->record_frame = meta_screen_cast_monitor_stream_src_record_frame;
|
||||
src_class->set_cursor_metadata =
|
||||
meta_screen_cast_monitor_stream_src_set_cursor_metadata;
|
||||
}
|
||||
|
@ -105,11 +105,12 @@ meta_screen_cast_monitor_stream_get_monitor (MetaScreenCastMonitorStream *monito
|
||||
}
|
||||
|
||||
MetaScreenCastMonitorStream *
|
||||
meta_screen_cast_monitor_stream_new (MetaScreenCastSession *session,
|
||||
GDBusConnection *connection,
|
||||
MetaMonitor *monitor,
|
||||
ClutterStage *stage,
|
||||
GError **error)
|
||||
meta_screen_cast_monitor_stream_new (MetaScreenCastSession *session,
|
||||
GDBusConnection *connection,
|
||||
MetaMonitor *monitor,
|
||||
ClutterStage *stage,
|
||||
MetaScreenCastCursorMode cursor_mode,
|
||||
GError **error)
|
||||
{
|
||||
MetaGpu *gpu = meta_monitor_get_gpu (monitor);
|
||||
MetaMonitorManager *monitor_manager = meta_gpu_get_monitor_manager (gpu);
|
||||
@ -126,6 +127,7 @@ meta_screen_cast_monitor_stream_new (MetaScreenCastSession *session,
|
||||
error,
|
||||
"session", session,
|
||||
"connection", connection,
|
||||
"cursor-mode", cursor_mode,
|
||||
"monitor", monitor,
|
||||
NULL);
|
||||
if (!monitor_stream)
|
||||
|
@ -35,11 +35,12 @@ G_DECLARE_FINAL_TYPE (MetaScreenCastMonitorStream,
|
||||
META, SCREEN_CAST_MONITOR_STREAM,
|
||||
MetaScreenCastStream)
|
||||
|
||||
MetaScreenCastMonitorStream * meta_screen_cast_monitor_stream_new (MetaScreenCastSession *session,
|
||||
GDBusConnection *connection,
|
||||
MetaMonitor *monitor,
|
||||
ClutterStage *stage,
|
||||
GError **error);
|
||||
MetaScreenCastMonitorStream * meta_screen_cast_monitor_stream_new (MetaScreenCastSession *session,
|
||||
GDBusConnection *connection,
|
||||
MetaMonitor *monitor,
|
||||
ClutterStage *stage,
|
||||
MetaScreenCastCursorMode cursor_mode,
|
||||
GError **error);
|
||||
|
||||
ClutterStage * meta_screen_cast_monitor_stream_get_stage (MetaScreenCastMonitorStream *monitor_stream);
|
||||
|
||||
|
@ -262,6 +262,20 @@ on_stream_closed (MetaScreenCastStream *stream,
|
||||
meta_screen_cast_session_close (session);
|
||||
}
|
||||
|
||||
static gboolean
|
||||
is_valid_cursor_mode (MetaScreenCastCursorMode cursor_mode)
|
||||
{
|
||||
switch (cursor_mode)
|
||||
{
|
||||
case META_SCREEN_CAST_CURSOR_MODE_HIDDEN:
|
||||
case META_SCREEN_CAST_CURSOR_MODE_EMBEDDED:
|
||||
case META_SCREEN_CAST_CURSOR_MODE_METADATA:
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
handle_record_monitor (MetaDBusScreenCastSession *skeleton,
|
||||
GDBusMethodInvocation *invocation,
|
||||
@ -275,6 +289,7 @@ handle_record_monitor (MetaDBusScreenCastSession *skeleton,
|
||||
MetaMonitorManager *monitor_manager =
|
||||
meta_backend_get_monitor_manager (backend);
|
||||
MetaMonitor *monitor;
|
||||
MetaScreenCastCursorMode cursor_mode;
|
||||
ClutterStage *stage;
|
||||
GError *error = NULL;
|
||||
MetaScreenCastMonitorStream *monitor_stream;
|
||||
@ -306,12 +321,28 @@ handle_record_monitor (MetaDBusScreenCastSession *skeleton,
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
if (!g_variant_lookup (properties_variant, "cursor-mode", "u", &cursor_mode))
|
||||
{
|
||||
cursor_mode = META_SCREEN_CAST_CURSOR_MODE_HIDDEN;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!is_valid_cursor_mode (cursor_mode))
|
||||
{
|
||||
g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR,
|
||||
G_DBUS_ERROR_FAILED,
|
||||
"Unknown cursor mode");
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
stage = CLUTTER_STAGE (meta_backend_get_stage (backend));
|
||||
|
||||
monitor_stream = meta_screen_cast_monitor_stream_new (session,
|
||||
connection,
|
||||
monitor,
|
||||
stage,
|
||||
cursor_mode,
|
||||
&error);
|
||||
if (!monitor_stream)
|
||||
{
|
||||
|
@ -40,6 +40,10 @@
|
||||
#define PRIVATE_OWNER_FROM_FIELD(TypeName, field_ptr, field_name) \
|
||||
(TypeName *)((guint8 *)(field_ptr) - G_PRIVATE_OFFSET (TypeName, field_name))
|
||||
|
||||
#define CURSOR_META_SIZE(width, height) \
|
||||
(sizeof (struct spa_meta_cursor) + \
|
||||
sizeof (struct spa_meta_bitmap) + width * height * 4)
|
||||
|
||||
enum
|
||||
{
|
||||
PROP_0,
|
||||
@ -57,14 +61,6 @@ enum
|
||||
|
||||
static guint signals[N_SIGNALS];
|
||||
|
||||
typedef struct _MetaSpaType
|
||||
{
|
||||
struct spa_type_media_type media_type;
|
||||
struct spa_type_media_subtype media_subtype;
|
||||
struct spa_type_format_video format_video;
|
||||
struct spa_type_video_format video_format;
|
||||
} MetaSpaType;
|
||||
|
||||
typedef struct _MetaPipeWireSource
|
||||
{
|
||||
GSource base;
|
||||
@ -133,14 +129,68 @@ meta_screen_cast_stream_src_get_videocrop (MetaScreenCastStreamSrc *src,
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static void
|
||||
static gboolean
|
||||
meta_screen_cast_stream_src_record_frame (MetaScreenCastStreamSrc *src,
|
||||
uint8_t *data)
|
||||
{
|
||||
MetaScreenCastStreamSrcClass *klass =
|
||||
META_SCREEN_CAST_STREAM_SRC_GET_CLASS (src);
|
||||
|
||||
klass->record_frame (src, data);
|
||||
return klass->record_frame (src, data);
|
||||
}
|
||||
|
||||
static void
|
||||
meta_screen_cast_stream_src_set_cursor_metadata (MetaScreenCastStreamSrc *src,
|
||||
struct spa_meta_cursor *spa_meta_cursor)
|
||||
{
|
||||
MetaScreenCastStreamSrcClass *klass =
|
||||
META_SCREEN_CAST_STREAM_SRC_GET_CLASS (src);
|
||||
|
||||
if (klass->set_cursor_metadata)
|
||||
klass->set_cursor_metadata (src, spa_meta_cursor);
|
||||
}
|
||||
|
||||
MetaSpaType *
|
||||
meta_screen_cast_stream_src_get_spa_type (MetaScreenCastStreamSrc *src)
|
||||
{
|
||||
MetaScreenCastStreamSrcPrivate *priv =
|
||||
meta_screen_cast_stream_src_get_instance_private (src);
|
||||
|
||||
return &priv->spa_type;
|
||||
}
|
||||
|
||||
static void
|
||||
add_cursor_metadata (MetaScreenCastStreamSrc *src,
|
||||
struct spa_buffer *spa_buffer)
|
||||
{
|
||||
MetaScreenCastStreamSrcPrivate *priv =
|
||||
meta_screen_cast_stream_src_get_instance_private (src);
|
||||
MetaSpaType *spa_type = &priv->spa_type;
|
||||
struct spa_meta_cursor *spa_meta_cursor;
|
||||
|
||||
spa_meta_cursor = spa_buffer_find_meta (spa_buffer, spa_type->meta_cursor);
|
||||
if (spa_meta_cursor)
|
||||
meta_screen_cast_stream_src_set_cursor_metadata (src, spa_meta_cursor);
|
||||
}
|
||||
|
||||
static void
|
||||
maybe_record_cursor (MetaScreenCastStreamSrc *src,
|
||||
struct spa_buffer *spa_buffer,
|
||||
uint8_t *data)
|
||||
{
|
||||
MetaScreenCastStream *stream = meta_screen_cast_stream_src_get_stream (src);
|
||||
|
||||
switch (meta_screen_cast_stream_get_cursor_mode (stream))
|
||||
{
|
||||
case META_SCREEN_CAST_CURSOR_MODE_HIDDEN:
|
||||
case META_SCREEN_CAST_CURSOR_MODE_EMBEDDED:
|
||||
return;
|
||||
case META_SCREEN_CAST_CURSOR_MODE_METADATA:
|
||||
add_cursor_metadata (src, spa_buffer);
|
||||
return;
|
||||
}
|
||||
|
||||
g_assert_not_reached ();
|
||||
}
|
||||
|
||||
void
|
||||
@ -151,7 +201,6 @@ meta_screen_cast_stream_src_maybe_record_frame (MetaScreenCastStreamSrc *src)
|
||||
MetaRectangle crop_rect;
|
||||
struct pw_buffer *buffer;
|
||||
struct spa_buffer *spa_buffer;
|
||||
struct spa_meta_video_crop *spa_meta_video_crop;
|
||||
uint8_t *map = NULL;
|
||||
uint8_t *data;
|
||||
uint64_t now_us;
|
||||
@ -199,35 +248,45 @@ meta_screen_cast_stream_src_maybe_record_frame (MetaScreenCastStreamSrc *src)
|
||||
return;
|
||||
}
|
||||
|
||||
meta_screen_cast_stream_src_record_frame (src, data);
|
||||
|
||||
/* Update VideoCrop if needed */
|
||||
spa_meta_video_crop = spa_buffer_find_meta (spa_buffer, priv->pipewire_type->meta.VideoCrop);
|
||||
if (spa_meta_video_crop)
|
||||
if (meta_screen_cast_stream_src_record_frame (src, data))
|
||||
{
|
||||
if (meta_screen_cast_stream_src_get_videocrop (src, &crop_rect))
|
||||
struct spa_meta_video_crop *spa_meta_video_crop;
|
||||
|
||||
spa_buffer->datas[0].chunk->size = spa_buffer->datas[0].maxsize;
|
||||
|
||||
/* Update VideoCrop if needed */
|
||||
spa_meta_video_crop =
|
||||
spa_buffer_find_meta (spa_buffer, priv->pipewire_type->meta.VideoCrop);
|
||||
if (spa_meta_video_crop)
|
||||
{
|
||||
spa_meta_video_crop->x = crop_rect.x;
|
||||
spa_meta_video_crop->y = crop_rect.y;
|
||||
spa_meta_video_crop->width = crop_rect.width;
|
||||
spa_meta_video_crop->height = crop_rect.height;
|
||||
}
|
||||
else
|
||||
{
|
||||
spa_meta_video_crop->x = 0;
|
||||
spa_meta_video_crop->y = 0;
|
||||
spa_meta_video_crop->width = priv->stream_width;
|
||||
spa_meta_video_crop->height = priv->stream_height;
|
||||
if (meta_screen_cast_stream_src_get_videocrop (src, &crop_rect))
|
||||
{
|
||||
spa_meta_video_crop->x = crop_rect.x;
|
||||
spa_meta_video_crop->y = crop_rect.y;
|
||||
spa_meta_video_crop->width = crop_rect.width;
|
||||
spa_meta_video_crop->height = crop_rect.height;
|
||||
}
|
||||
else
|
||||
{
|
||||
spa_meta_video_crop->x = 0;
|
||||
spa_meta_video_crop->y = 0;
|
||||
spa_meta_video_crop->width = priv->stream_width;
|
||||
spa_meta_video_crop->height = priv->stream_height;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
spa_buffer->datas[0].chunk->size = 0;
|
||||
}
|
||||
|
||||
maybe_record_cursor (src, spa_buffer, data);
|
||||
|
||||
priv->last_frame_timestamp_us = now_us;
|
||||
|
||||
if (map)
|
||||
munmap (map, spa_buffer->datas[0].maxsize + spa_buffer->datas[0].mapoffset);
|
||||
|
||||
spa_buffer->datas[0].chunk->size = spa_buffer->datas[0].maxsize;
|
||||
|
||||
pw_stream_queue_buffer (priv->pipewire_stream, buffer);
|
||||
}
|
||||
|
||||
@ -314,7 +373,7 @@ on_stream_format_changed (void *data,
|
||||
uint8_t params_buffer[1024];
|
||||
int32_t width, height, stride, size;
|
||||
struct spa_pod_builder pod_builder;
|
||||
const struct spa_pod *params[2];
|
||||
const struct spa_pod *params[3];
|
||||
const int bpp = 4;
|
||||
|
||||
if (!format)
|
||||
@ -348,6 +407,12 @@ on_stream_format_changed (void *data,
|
||||
":", pipewire_type->param_meta.type, "I", pipewire_type->meta.VideoCrop,
|
||||
":", pipewire_type->param_meta.size, "i", sizeof (struct spa_meta_video_crop));
|
||||
|
||||
params[2] = spa_pod_builder_object (
|
||||
&pod_builder,
|
||||
pipewire_type->param.idMeta, pipewire_type->param_meta.Meta,
|
||||
":", pipewire_type->param_meta.type, "I", priv->spa_type.meta_cursor,
|
||||
":", pipewire_type->param_meta.size, "i", CURSOR_META_SIZE (64, 64));
|
||||
|
||||
pw_stream_finish_format (priv->pipewire_stream, 0,
|
||||
params, G_N_ELEMENTS (params));
|
||||
}
|
||||
@ -517,6 +582,7 @@ init_spa_type (MetaSpaType *type,
|
||||
spa_type_media_subtype_map (map, &type->media_subtype);
|
||||
spa_type_format_video_map (map, &type->format_video);
|
||||
spa_type_video_format_map (map, &type->video_format);
|
||||
type->meta_cursor = spa_type_map_get_id(map, SPA_TYPE_META__Cursor);
|
||||
}
|
||||
|
||||
static MetaPipeWireSource *
|
||||
|
@ -24,10 +24,26 @@
|
||||
#define META_SCREEN_CAST_STREAM_SRC_H
|
||||
|
||||
#include <glib-object.h>
|
||||
#include <spa/param/video/format-utils.h>
|
||||
#include <spa/buffer/meta.h>
|
||||
|
||||
#include "backends/meta-backend-private.h"
|
||||
#include "backends/meta-cursor-renderer.h"
|
||||
#include "backends/meta-cursor.h"
|
||||
#include "backends/meta-renderer.h"
|
||||
#include "clutter/clutter.h"
|
||||
#include "cogl/cogl.h"
|
||||
#include "meta/boxes.h"
|
||||
|
||||
typedef struct _MetaSpaType
|
||||
{
|
||||
struct spa_type_media_type media_type;
|
||||
struct spa_type_media_subtype media_subtype;
|
||||
struct spa_type_format_video format_video;
|
||||
struct spa_type_video_format video_format;
|
||||
uint32_t meta_cursor;
|
||||
} MetaSpaType;
|
||||
|
||||
typedef struct _MetaScreenCastStream MetaScreenCastStream;
|
||||
|
||||
#define META_TYPE_SCREEN_CAST_STREAM_SRC (meta_screen_cast_stream_src_get_type ())
|
||||
@ -46,14 +62,18 @@ struct _MetaScreenCastStreamSrcClass
|
||||
float *frame_rate);
|
||||
void (* enable) (MetaScreenCastStreamSrc *src);
|
||||
void (* disable) (MetaScreenCastStreamSrc *src);
|
||||
void (* record_frame) (MetaScreenCastStreamSrc *src,
|
||||
uint8_t *data);
|
||||
gboolean (* record_frame) (MetaScreenCastStreamSrc *src,
|
||||
uint8_t *data);
|
||||
gboolean (* get_videocrop) (MetaScreenCastStreamSrc *src,
|
||||
MetaRectangle *crop_rect);
|
||||
void (* set_cursor_metadata) (MetaScreenCastStreamSrc *src,
|
||||
struct spa_meta_cursor *spa_meta_cursor);
|
||||
};
|
||||
|
||||
void meta_screen_cast_stream_src_maybe_record_frame (MetaScreenCastStreamSrc *src);
|
||||
|
||||
MetaScreenCastStream * meta_screen_cast_stream_src_get_stream (MetaScreenCastStreamSrc *src);
|
||||
|
||||
MetaSpaType * meta_screen_cast_stream_src_get_spa_type (MetaScreenCastStreamSrc *src);
|
||||
|
||||
#endif /* META_SCREEN_CAST_STREAM_SRC_H */
|
||||
|
@ -34,6 +34,7 @@ enum
|
||||
|
||||
PROP_SESSION,
|
||||
PROP_CONNECTION,
|
||||
PROP_CURSOR_MODE,
|
||||
};
|
||||
|
||||
enum
|
||||
@ -52,6 +53,8 @@ typedef struct _MetaScreenCastStreamPrivate
|
||||
GDBusConnection *connection;
|
||||
char *object_path;
|
||||
|
||||
MetaScreenCastCursorMode cursor_mode;
|
||||
|
||||
MetaScreenCastStreamSrc *src;
|
||||
} MetaScreenCastStreamPrivate;
|
||||
|
||||
@ -164,6 +167,15 @@ meta_screen_cast_stream_transform_position (MetaScreenCastStream *stream,
|
||||
y);
|
||||
}
|
||||
|
||||
MetaScreenCastCursorMode
|
||||
meta_screen_cast_stream_get_cursor_mode (MetaScreenCastStream *stream)
|
||||
{
|
||||
MetaScreenCastStreamPrivate *priv =
|
||||
meta_screen_cast_stream_get_instance_private (stream);
|
||||
|
||||
return priv->cursor_mode;
|
||||
}
|
||||
|
||||
static void
|
||||
meta_screen_cast_stream_set_property (GObject *object,
|
||||
guint prop_id,
|
||||
@ -182,6 +194,9 @@ meta_screen_cast_stream_set_property (GObject *object,
|
||||
case PROP_CONNECTION:
|
||||
priv->connection = g_value_get_object (value);
|
||||
break;
|
||||
case PROP_CURSOR_MODE:
|
||||
priv->cursor_mode = g_value_get_uint (value);
|
||||
break;
|
||||
default:
|
||||
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
|
||||
}
|
||||
@ -205,6 +220,9 @@ meta_screen_cast_stream_get_property (GObject *object,
|
||||
case PROP_CONNECTION:
|
||||
g_value_set_object (value, priv->connection);
|
||||
break;
|
||||
case PROP_CURSOR_MODE:
|
||||
g_value_set_uint (value, priv->cursor_mode);
|
||||
break;
|
||||
default:
|
||||
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
|
||||
}
|
||||
@ -296,6 +314,18 @@ meta_screen_cast_stream_class_init (MetaScreenCastStreamClass *klass)
|
||||
G_PARAM_CONSTRUCT_ONLY |
|
||||
G_PARAM_STATIC_STRINGS));
|
||||
|
||||
g_object_class_install_property (object_class,
|
||||
PROP_CURSOR_MODE,
|
||||
g_param_spec_uint ("cursor-mode",
|
||||
"cursor-mode",
|
||||
"Cursor mode",
|
||||
META_SCREEN_CAST_CURSOR_MODE_HIDDEN,
|
||||
META_SCREEN_CAST_CURSOR_MODE_METADATA,
|
||||
META_SCREEN_CAST_CURSOR_MODE_HIDDEN,
|
||||
G_PARAM_READWRITE |
|
||||
G_PARAM_CONSTRUCT_ONLY |
|
||||
G_PARAM_STATIC_STRINGS));
|
||||
|
||||
signals[CLOSED] = g_signal_new ("closed",
|
||||
G_TYPE_FROM_CLASS (klass),
|
||||
G_SIGNAL_RUN_LAST,
|
||||
|
@ -65,4 +65,6 @@ void meta_screen_cast_stream_transform_position (MetaScreenCastStream *stream,
|
||||
double *x,
|
||||
double *y);
|
||||
|
||||
MetaScreenCastCursorMode meta_screen_cast_stream_get_cursor_mode (MetaScreenCastStream *stream);
|
||||
|
||||
#endif /* META_SCREEN_CAST_STREAM_H */
|
||||
|
@ -207,7 +207,7 @@ meta_screen_cast_window_stream_src_disable (MetaScreenCastStreamSrc *src)
|
||||
meta_screen_cast_window_stream_src_stop (window_src);
|
||||
}
|
||||
|
||||
static void
|
||||
static gboolean
|
||||
meta_screen_cast_window_stream_src_record_frame (MetaScreenCastStreamSrc *src,
|
||||
uint8_t *data)
|
||||
{
|
||||
@ -215,6 +215,8 @@ meta_screen_cast_window_stream_src_record_frame (MetaScreenCastStreamSrc *src,
|
||||
META_SCREEN_CAST_WINDOW_STREAM_SRC (src);
|
||||
|
||||
capture_into (window_src, data);
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
MetaScreenCastWindowStreamSrc *
|
||||
|
@ -30,6 +30,13 @@
|
||||
|
||||
#include "meta-dbus-screen-cast.h"
|
||||
|
||||
typedef enum _MetaScreenCastCursorMode
|
||||
{
|
||||
META_SCREEN_CAST_CURSOR_MODE_HIDDEN = 0,
|
||||
META_SCREEN_CAST_CURSOR_MODE_EMBEDDED = 1,
|
||||
META_SCREEN_CAST_CURSOR_MODE_METADATA = 2,
|
||||
} MetaScreenCastCursorMode;
|
||||
|
||||
#define META_TYPE_SCREEN_CAST (meta_screen_cast_get_type ())
|
||||
G_DECLARE_FINAL_TYPE (MetaScreenCast, meta_screen_cast,
|
||||
META, SCREEN_CAST,
|
||||
|
@ -71,7 +71,15 @@
|
||||
|
||||
Record a single monitor.
|
||||
|
||||
Available @properties include: (none)
|
||||
Available @properties include:
|
||||
|
||||
* "cursor-mode" (u): Cursor mode. Default: 'hidden' (see below)
|
||||
|
||||
Available cursor mode values:
|
||||
|
||||
0: hidden - cursor is not included in the stream
|
||||
1: embedded - cursor is included in the framebuffer
|
||||
2: metadata - cursor is included as metadata in the PipeWire stream
|
||||
-->
|
||||
<method name="RecordMonitor">
|
||||
<arg name="connector" type="s" direction="in" />
|
||||
@ -84,7 +92,7 @@
|
||||
@properties: Properties used determining what window to select
|
||||
@stream_path: Path to the new stream object
|
||||
|
||||
Record a single window.
|
||||
Record a single window. The cursor will not be included.
|
||||
|
||||
Available @properties include:
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user