gnome-shell/src/shell-recorder.c
Jonas Ådahl 47ea10b7c9 Remove usage of MetaScreen
Remove any usage of MetaScreen, as it has been removed from libmutter
in the API version 3. The corresponding functionality has been moved
into three different places: MetaDisplay, MetaX11Display (for X11
specific functionality) and MetaWorkspaceManager.

https://bugzilla.gnome.org/show_bug.cgi?id=759538
2018-07-06 19:56:19 +02:00

1626 lines
48 KiB
C

/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
#include "config.h"
#include <fcntl.h>
#include <math.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#define GST_USE_UNSTABLE_API
#include <gst/gst.h>
#include <gtk/gtk.h>
#include <gdk/gdk.h>
#include <cogl/cogl.h>
#include <meta/meta-cursor-tracker.h>
#include <meta/display.h>
#include <meta/compositor-mutter.h>
#include "shell-global.h"
#include "shell-recorder-src.h"
#include "shell-recorder.h"
#include "shell-util.h"
#define A11Y_APPS_SCHEMA "org.gnome.desktop.a11y.applications"
#define MAGNIFIER_ACTIVE_KEY "screen-magnifier-enabled"
typedef enum {
RECORDER_STATE_CLOSED,
RECORDER_STATE_RECORDING
} RecorderState;
typedef struct _RecorderPipeline RecorderPipeline;
struct _ShellRecorder {
GObject parent;
/* A "maximum" amount of memory to use for buffering. This is used
* to alert the user that they are filling up memory rather than
* any that actually affects recording. (In kB)
*/
guint memory_target;
guint memory_used; /* Current memory used. (In kB) */
RecorderState state;
ClutterStage *stage;
gboolean custom_area;
cairo_rectangle_int_t area;
int stage_width;
int stage_height;
GdkScreen *gdk_screen;
int pointer_x;
int pointer_y;
GSettings *a11y_settings;
gboolean draw_cursor;
MetaCursorTracker *cursor_tracker;
cairo_surface_t *cursor_image;
guint8 *cursor_memory;
int cursor_hot_x;
int cursor_hot_y;
int framerate;
char *pipeline_description;
char *file_template;
/* We might have multiple pipelines that are finishing encoding
* to go along with the current pipeline where we are recording.
*/
RecorderPipeline *current_pipeline; /* current pipeline */
GSList *pipelines; /* all pipelines */
GstClockTime last_frame_time; /* Timestamp for the last frame */
/* GSource IDs for different timeouts and idles */
guint redraw_timeout;
guint redraw_idle;
guint update_memory_used_timeout;
guint update_pointer_timeout;
guint repaint_hook_id;
};
struct _RecorderPipeline
{
ShellRecorder *recorder;
GstElement *pipeline;
GstElement *src;
int outfile;
char *filename;
};
static void recorder_set_stage (ShellRecorder *recorder,
ClutterStage *stage);
static void recorder_set_framerate (ShellRecorder *recorder,
int framerate);
static void recorder_set_pipeline (ShellRecorder *recorder,
const char *pipeline);
static void recorder_set_file_template (ShellRecorder *recorder,
const char *file_template);
static void recorder_set_draw_cursor (ShellRecorder *recorder,
gboolean draw_cursor);
static void recorder_pipeline_set_caps (RecorderPipeline *pipeline);
static void recorder_pipeline_closed (RecorderPipeline *pipeline);
static void recorder_remove_redraw_timeout (ShellRecorder *recorder);
enum {
PROP_0,
PROP_DISPLAY,
PROP_STAGE,
PROP_FRAMERATE,
PROP_PIPELINE,
PROP_FILE_TEMPLATE,
PROP_DRAW_CURSOR
};
G_DEFINE_TYPE(ShellRecorder, shell_recorder, G_TYPE_OBJECT);
/* The default value of the target frame rate; we'll never record more
* than this many frames per second, though we may record less if the
* screen isn't being redrawn. 30 is a compromise between smoothness
* and the size of the recording.
*/
#define DEFAULT_FRAMES_PER_SECOND 30
/* The time (in milliseconds) between querying the server for the cursor
* position.
*/
#define UPDATE_POINTER_TIME 100
/* The time we wait (in milliseconds) before redrawing when the memory used
* changes.
*/
#define UPDATE_MEMORY_USED_DELAY 500
/* Maximum time between frames, in milliseconds. If we don't send data
* for a long period of time, then when we send the next frame, a lot
* of work can be created for the encoder to do, so we want to force a
* periodic redraw when nothing happen.
*/
#define MAXIMUM_PAUSE_TIME 1000
/* The default pipeline.
*/
#define DEFAULT_PIPELINE "vp9enc min_quantizer=13 max_quantizer=13 cpu-used=5 deadline=1000000 threads=%T ! queue ! webmmux"
/* If we can find the amount of memory on the machine, we use half
* of that for memory_target, otherwise, we use this value, in kB.
*/
#define DEFAULT_MEMORY_TARGET (512*1024)
static guint
get_memory_target (void)
{
FILE *f;
/* Really simple "get amount of memory on the machine" if it
* doesn't work, you just get the default memory target.
*/
f = fopen("/proc/meminfo", "r");
if (!f)
return DEFAULT_MEMORY_TARGET;
while (!feof(f))
{
gchar line_buffer[1024];
guint mem_total;
if (fscanf(f, "MemTotal: %u", &mem_total) == 1)
{
fclose(f);
return mem_total / 2;
}
/* Skip to the next line and discard what we read */
if (fgets(line_buffer, sizeof(line_buffer), f) == NULL)
break;
}
fclose(f);
return DEFAULT_MEMORY_TARGET;
}
/*
* Used to force full stage redraws during recording to avoid artifacts
*
* Note: That this will cause the stage to be repainted on
* every animation frame even if the frame wouldn't normally cause any new
* drawing
*/
static gboolean
recorder_repaint_hook (gpointer data)
{
ClutterActor *stage = data;
clutter_actor_queue_redraw (stage);
return TRUE;
}
static void
shell_recorder_init (ShellRecorder *recorder)
{
/* Calling gst_init() is a no-op if GStreamer was previously initialized */
gst_init (NULL, NULL);
shell_recorder_src_register ();
recorder->gdk_screen = gdk_screen_get_default ();
recorder->memory_target = get_memory_target();
recorder->a11y_settings = g_settings_new (A11Y_APPS_SCHEMA);
recorder->state = RECORDER_STATE_CLOSED;
recorder->framerate = DEFAULT_FRAMES_PER_SECOND;
recorder->draw_cursor = TRUE;
}
static void
shell_recorder_finalize (GObject *object)
{
ShellRecorder *recorder = SHELL_RECORDER (object);
if (recorder->update_memory_used_timeout)
g_source_remove (recorder->update_memory_used_timeout);
if (recorder->cursor_image)
cairo_surface_destroy (recorder->cursor_image);
if (recorder->cursor_memory)
g_free (recorder->cursor_memory);
recorder_set_stage (recorder, NULL);
recorder_set_pipeline (recorder, NULL);
recorder_set_file_template (recorder, NULL);
recorder_remove_redraw_timeout (recorder);
g_clear_object (&recorder->a11y_settings);
G_OBJECT_CLASS (shell_recorder_parent_class)->finalize (object);
}
static void
recorder_on_stage_destroy (ClutterActor *actor,
ShellRecorder *recorder)
{
recorder_set_stage (recorder, NULL);
}
/* Add together the memory used by all pipelines; both the
* currently recording pipeline and pipelines finishing
* recording asynchronously.
*/
static void
recorder_update_memory_used (ShellRecorder *recorder,
gboolean repaint)
{
guint memory_used = 0;
GSList *l;
for (l = recorder->pipelines; l; l = l->next)
{
RecorderPipeline *pipeline = l->data;
guint pipeline_memory_used;
g_object_get (pipeline->src,
"memory-used", &pipeline_memory_used,
NULL);
memory_used += pipeline_memory_used;
}
if (memory_used != recorder->memory_used)
recorder->memory_used = memory_used;
}
/* Timeout used to avoid not drawing for more than MAXIMUM_PAUSE_TIME
*/
static gboolean
recorder_redraw_timeout (gpointer data)
{
ShellRecorder *recorder = data;
recorder->redraw_timeout = 0;
clutter_actor_queue_redraw (CLUTTER_ACTOR (recorder->stage));
return FALSE;
}
static void
recorder_add_redraw_timeout (ShellRecorder *recorder)
{
if (recorder->redraw_timeout == 0)
{
recorder->redraw_timeout = g_timeout_add (MAXIMUM_PAUSE_TIME,
recorder_redraw_timeout,
recorder);
g_source_set_name_by_id (recorder->redraw_timeout, "[gnome-shell] recorder_redraw_timeout");
}
}
static void
recorder_remove_redraw_timeout (ShellRecorder *recorder)
{
if (recorder->redraw_timeout != 0)
{
g_source_remove (recorder->redraw_timeout);
recorder->redraw_timeout = 0;
}
}
static void
recorder_fetch_cursor_image (ShellRecorder *recorder)
{
CoglTexture *texture;
int width, height;
int stride;
guint8 *data;
texture = meta_cursor_tracker_get_sprite (recorder->cursor_tracker);
if (!texture)
return;
meta_cursor_tracker_get_hot (recorder->cursor_tracker,
&recorder->cursor_hot_x, &recorder->cursor_hot_y);
width = cogl_texture_get_width (texture);
height = cogl_texture_get_height (texture);
stride = 4 * width;
data = g_new (guint8, stride * height);
cogl_texture_get_data (texture, CLUTTER_CAIRO_FORMAT_ARGB32, stride, data);
/* FIXME: cairo-gl? */
recorder->cursor_image = cairo_image_surface_create_for_data (data,
CAIRO_FORMAT_ARGB32,
width, height,
stride);
recorder->cursor_memory = data;
}
/* Overlay the cursor image on the frame. We draw the cursor image
* into the host-memory buffer after we've captured the frame. An
* alternate approach would be to turn off the cursor while recording
* and draw the cursor ourselves with GL, but then we'd need to figure
* out what the cursor looks like, or hard-code a non-system cursor.
*/
static void
recorder_draw_cursor (ShellRecorder *recorder,
GstBuffer *buffer)
{
GstMapInfo info;
cairo_surface_t *surface;
cairo_t *cr;
/* We don't show a cursor unless the hot spot is in the frame; this
* means that sometimes we aren't going to draw a cursor even when
* there is a little bit overlapping within the stage */
if (recorder->pointer_x < recorder->area.x ||
recorder->pointer_y < recorder->area.y ||
recorder->pointer_x >= recorder->area.x + recorder->area.width ||
recorder->pointer_y >= recorder->area.y + recorder->area.height)
return;
if (!recorder->cursor_image)
recorder_fetch_cursor_image (recorder);
if (!recorder->cursor_image)
return;
gst_buffer_map (buffer, &info, GST_MAP_WRITE);
surface = cairo_image_surface_create_for_data (info.data,
CAIRO_FORMAT_ARGB32,
recorder->area.width,
recorder->area.height,
recorder->area.width * 4);
cr = cairo_create (surface);
cairo_set_source_surface (cr,
recorder->cursor_image,
recorder->pointer_x - recorder->cursor_hot_x - recorder->area.x,
recorder->pointer_y - recorder->cursor_hot_y - recorder->area.y);
cairo_paint (cr);
cairo_destroy (cr);
cairo_surface_destroy (surface);
gst_buffer_unmap (buffer, &info);
}
/* Retrieve a frame and feed it into the pipeline
*/
static void
recorder_record_frame (ShellRecorder *recorder,
gboolean paint)
{
GstBuffer *buffer;
ClutterCapture *captures;
int n_captures;
cairo_surface_t *image;
guint size;
uint8_t *data;
GstMemory *memory;
int i;
GstClock *clock;
GstClockTime now, base_time;
g_return_if_fail (recorder->current_pipeline != NULL);
/* If we get into the red zone, stop buffering new frames; 13/16 is
* a bit more than the 3/4 threshold for a red indicator to keep the
* indicator from flashing between red and yellow. */
if (recorder->memory_used > (recorder->memory_target * 13) / 16)
return;
/* Drop frames to get down to something like the target frame rate; since frames
* are generated with VBlank sync, we don't have full control anyways, so we just
* drop frames if the interval since the last frame is less than 75% of the
* desired inter-frame interval.
*/
clock = gst_element_get_clock (recorder->current_pipeline->src);
/* If we have no clock yet, the pipeline is not yet in PLAYING */
if (!clock)
return;
base_time = gst_element_get_base_time (recorder->current_pipeline->src);
now = gst_clock_get_time (clock) - base_time;
gst_object_unref (clock);
if (GST_CLOCK_TIME_IS_VALID (recorder->last_frame_time) &&
now - recorder->last_frame_time < gst_util_uint64_scale_int (GST_SECOND, 3, 4 * recorder->framerate))
return;
recorder->last_frame_time = now;
clutter_stage_capture (recorder->stage, paint, &recorder->area,
&captures, &n_captures);
if (n_captures == 0)
return;
if (n_captures == 1)
image = cairo_surface_reference (captures[0].image);
else
image = shell_util_composite_capture_images (captures,
n_captures,
recorder->area.x,
recorder->area.y,
recorder->area.width,
recorder->area.height);
data = cairo_image_surface_get_data (image);
size = (cairo_image_surface_get_height (image) *
cairo_image_surface_get_stride (image));
for (i = 0; i < n_captures; i++)
cairo_surface_destroy (captures[i].image);
g_free (captures);
buffer = gst_buffer_new();
memory = gst_memory_new_wrapped (0, data, size, 0, size,
image,
(GDestroyNotify) cairo_surface_destroy);
gst_buffer_insert_memory (buffer, -1, memory);
GST_BUFFER_PTS(buffer) = now;
if (recorder->draw_cursor &&
!g_settings_get_boolean (recorder->a11y_settings, MAGNIFIER_ACTIVE_KEY))
recorder_draw_cursor (recorder, buffer);
shell_recorder_src_add_buffer (SHELL_RECORDER_SRC (recorder->current_pipeline->src), buffer);
gst_buffer_unref (buffer);
/* Reset the timeout that we used to avoid an overlong pause in the stream */
recorder_remove_redraw_timeout (recorder);
recorder_add_redraw_timeout (recorder);
}
/* We hook in by recording each frame right after the stage is painted
* by clutter before glSwapBuffers() makes it visible to the user.
*/
static void
recorder_on_stage_paint (ClutterActor *actor,
ShellRecorder *recorder)
{
if (recorder->state == RECORDER_STATE_RECORDING)
recorder_record_frame (recorder, FALSE);
}
static void
recorder_update_size (ShellRecorder *recorder)
{
ClutterActorBox allocation;
clutter_actor_get_allocation_box (CLUTTER_ACTOR (recorder->stage), &allocation);
recorder->stage_width = (int)(0.5 + allocation.x2 - allocation.x1);
recorder->stage_height = (int)(0.5 + allocation.y2 - allocation.y1);
if (!recorder->custom_area)
{
recorder->area.x = 0;
recorder->area.y = 0;
recorder->area.width = recorder->stage_width;
recorder->area.height = recorder->stage_height;
}
}
static void
recorder_on_stage_notify_size (GObject *object,
GParamSpec *pspec,
ShellRecorder *recorder)
{
recorder_update_size (recorder);
/* This breaks the recording but tweaking the GStreamer pipeline a bit
* might make it work, at least if the codec can handle a stream where
* the frame size changes in the middle.
*/
if (recorder->current_pipeline)
recorder_pipeline_set_caps (recorder->current_pipeline);
}
static gboolean
recorder_idle_redraw (gpointer data)
{
ShellRecorder *recorder = data;
recorder->redraw_idle = 0;
clutter_actor_queue_redraw (CLUTTER_ACTOR (recorder->stage));
return FALSE;
}
static void
recorder_queue_redraw (ShellRecorder *recorder)
{
/* If we just queue a redraw on every mouse motion (for example), we
* starve Clutter, which operates at a very low priority. So
* we need to queue a "low priority redraw" after timeline updates
*/
if (recorder->state == RECORDER_STATE_RECORDING && recorder->redraw_idle == 0)
{
recorder->redraw_idle = g_idle_add_full (CLUTTER_PRIORITY_REDRAW + 1,
recorder_idle_redraw, recorder, NULL);
g_source_set_name_by_id (recorder->redraw_idle, "[gnome-shell] recorder_idle_redraw");
}
}
static void
on_cursor_changed (MetaCursorTracker *tracker,
ShellRecorder *recorder)
{
if (recorder->cursor_image)
{
cairo_surface_destroy (recorder->cursor_image);
recorder->cursor_image = NULL;
}
if (recorder->cursor_memory)
{
g_free (recorder->cursor_memory);
recorder->cursor_memory = NULL;
}
recorder_queue_redraw (recorder);
}
static void
recorder_update_pointer (ShellRecorder *recorder)
{
int pointer_x, pointer_y;
meta_cursor_tracker_get_pointer (recorder->cursor_tracker, &pointer_x, &pointer_y, NULL);
if (pointer_x != recorder->pointer_x || pointer_y != recorder->pointer_y)
{
recorder->pointer_x = pointer_x;
recorder->pointer_y = pointer_y;
recorder_queue_redraw (recorder);
}
}
static gboolean
recorder_update_pointer_timeout (gpointer data)
{
recorder_update_pointer (data);
return TRUE;
}
static void
recorder_add_update_pointer_timeout (ShellRecorder *recorder)
{
if (!recorder->update_pointer_timeout)
{
recorder->update_pointer_timeout = g_timeout_add (UPDATE_POINTER_TIME,
recorder_update_pointer_timeout,
recorder);
g_source_set_name_by_id (recorder->update_pointer_timeout, "[gnome-shell] recorder_update_pointer_timeout");
}
}
static void
recorder_remove_update_pointer_timeout (ShellRecorder *recorder)
{
if (recorder->update_pointer_timeout)
{
g_source_remove (recorder->update_pointer_timeout);
recorder->update_pointer_timeout = 0;
}
}
static void
recorder_connect_stage_callbacks (ShellRecorder *recorder)
{
g_signal_connect (recorder->stage, "destroy",
G_CALLBACK (recorder_on_stage_destroy), recorder);
g_signal_connect_after (recorder->stage, "paint",
G_CALLBACK (recorder_on_stage_paint), recorder);
g_signal_connect (recorder->stage, "notify::width",
G_CALLBACK (recorder_on_stage_notify_size), recorder);
g_signal_connect (recorder->stage, "notify::height",
G_CALLBACK (recorder_on_stage_notify_size), recorder);
}
static void
recorder_disconnect_stage_callbacks (ShellRecorder *recorder)
{
g_signal_handlers_disconnect_by_func (recorder->stage,
(void *)recorder_on_stage_destroy,
recorder);
g_signal_handlers_disconnect_by_func (recorder->stage,
(void *)recorder_on_stage_paint,
recorder);
g_signal_handlers_disconnect_by_func (recorder->stage,
(void *)recorder_on_stage_notify_size,
recorder);
/* We don't don't deselect for cursor changes in case someone else just
* happened to be selecting for cursor events on the same window; sending
* us the events is close to free in any case.
*/
if (recorder->redraw_idle)
{
g_source_remove (recorder->redraw_idle);
recorder->redraw_idle = 0;
}
}
static void
recorder_set_stage (ShellRecorder *recorder,
ClutterStage *stage)
{
if (recorder->stage == stage)
return;
if (recorder->current_pipeline)
shell_recorder_close (recorder);
if (recorder->stage)
recorder_disconnect_stage_callbacks (recorder);
recorder->stage = stage;
if (recorder->stage)
recorder_update_size (recorder);
}
static void
recorder_set_display (ShellRecorder *recorder,
MetaDisplay *display)
{
MetaCursorTracker *tracker;
tracker = meta_cursor_tracker_get_for_display (display);
if (tracker == recorder->cursor_tracker)
return;
recorder->cursor_tracker = tracker;
g_signal_connect_object (tracker, "cursor-changed",
G_CALLBACK (on_cursor_changed), recorder, 0);
}
static void
recorder_set_framerate (ShellRecorder *recorder,
int framerate)
{
if (framerate == recorder->framerate)
return;
if (recorder->current_pipeline)
shell_recorder_close (recorder);
recorder->framerate = framerate;
g_object_notify (G_OBJECT (recorder), "framerate");
}
static void
recorder_set_pipeline (ShellRecorder *recorder,
const char *pipeline)
{
if (pipeline == recorder->pipeline_description ||
(pipeline && recorder->pipeline_description && strcmp (recorder->pipeline_description, pipeline) == 0))
return;
if (recorder->current_pipeline)
shell_recorder_close (recorder);
if (recorder->pipeline_description)
g_free (recorder->pipeline_description);
recorder->pipeline_description = g_strdup (pipeline);
g_object_notify (G_OBJECT (recorder), "pipeline");
}
static void
recorder_set_file_template (ShellRecorder *recorder,
const char *file_template)
{
if (file_template == recorder->file_template ||
(file_template && recorder->file_template && strcmp (recorder->file_template, file_template) == 0))
return;
if (recorder->current_pipeline)
shell_recorder_close (recorder);
if (recorder->file_template)
g_free (recorder->file_template);
recorder->file_template = g_strdup (file_template);
g_object_notify (G_OBJECT (recorder), "file-template");
}
static void
recorder_set_draw_cursor (ShellRecorder *recorder,
gboolean draw_cursor)
{
if (draw_cursor == recorder->draw_cursor)
return;
recorder->draw_cursor = draw_cursor;
g_object_notify (G_OBJECT (recorder), "draw-cursor");
}
static void
shell_recorder_set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
ShellRecorder *recorder = SHELL_RECORDER (object);
switch (prop_id)
{
case PROP_DISPLAY:
recorder_set_display (recorder, g_value_get_object (value));
break;
case PROP_STAGE:
recorder_set_stage (recorder, g_value_get_object (value));
break;
case PROP_FRAMERATE:
recorder_set_framerate (recorder, g_value_get_int (value));
break;
case PROP_PIPELINE:
recorder_set_pipeline (recorder, g_value_get_string (value));
break;
case PROP_FILE_TEMPLATE:
recorder_set_file_template (recorder, g_value_get_string (value));
break;
case PROP_DRAW_CURSOR:
recorder_set_draw_cursor (recorder, g_value_get_boolean (value));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
shell_recorder_get_property (GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
ShellRecorder *recorder = SHELL_RECORDER (object);
switch (prop_id)
{
case PROP_STAGE:
g_value_set_object (value, G_OBJECT (recorder->stage));
break;
case PROP_FRAMERATE:
g_value_set_int (value, recorder->framerate);
break;
case PROP_PIPELINE:
g_value_set_string (value, recorder->pipeline_description);
break;
case PROP_FILE_TEMPLATE:
g_value_set_string (value, recorder->file_template);
break;
case PROP_DRAW_CURSOR:
g_value_set_boolean (value, recorder->draw_cursor);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
shell_recorder_class_init (ShellRecorderClass *klass)
{
GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
gobject_class->finalize = shell_recorder_finalize;
gobject_class->get_property = shell_recorder_get_property;
gobject_class->set_property = shell_recorder_set_property;
g_object_class_install_property (gobject_class,
PROP_DISPLAY,
g_param_spec_object ("display",
"Display",
"Display to record",
META_TYPE_DISPLAY,
G_PARAM_WRITABLE));
g_object_class_install_property (gobject_class,
PROP_STAGE,
g_param_spec_object ("stage",
"Stage",
"Stage to record",
CLUTTER_TYPE_STAGE,
G_PARAM_READWRITE));
g_object_class_install_property (gobject_class,
PROP_FRAMERATE,
g_param_spec_int ("framerate",
"Framerate",
"Framerate used for resulting video in frames-per-second",
0,
G_MAXINT,
DEFAULT_FRAMES_PER_SECOND,
G_PARAM_READWRITE));
g_object_class_install_property (gobject_class,
PROP_PIPELINE,
g_param_spec_string ("pipeline",
"Pipeline",
"GStreamer pipeline description to encode recordings",
NULL,
G_PARAM_READWRITE));
g_object_class_install_property (gobject_class,
PROP_FILE_TEMPLATE,
g_param_spec_string ("file-template",
"File Template",
"The filename template to use for output files",
NULL,
G_PARAM_READWRITE));
g_object_class_install_property (gobject_class,
PROP_DRAW_CURSOR,
g_param_spec_boolean ("draw-cursor",
"Draw Cursor",
"Whether to record the cursor",
TRUE,
G_PARAM_READWRITE));
}
/* Sets the GstCaps (video format, in this case) on the stream
*/
static void
recorder_pipeline_set_caps (RecorderPipeline *pipeline)
{
GstCaps *caps;
/* The data is always native-endian xRGB; videoconvert
* doesn't support little-endian xRGB, but does support
* big-endian BGRx.
*/
caps = gst_caps_new_simple ("video/x-raw",
#if G_BYTE_ORDER == G_LITTLE_ENDIAN
"format", G_TYPE_STRING, "BGRx",
#else
"format", G_TYPE_STRING, "xRGB",
#endif
"framerate", GST_TYPE_FRACTION, pipeline->recorder->framerate, 1,
"width", G_TYPE_INT, pipeline->recorder->area.width,
"height", G_TYPE_INT, pipeline->recorder->area.height,
NULL);
g_object_set (pipeline->src, "caps", caps, NULL);
gst_caps_unref (caps);
}
/* Augments the supplied pipeline with the source elements: the actual
* ShellRecorderSrc element where we inject frames then additional elements
* to convert the output into something palatable.
*/
static gboolean
recorder_pipeline_add_source (RecorderPipeline *pipeline)
{
GstPad *sink_pad = NULL, *src_pad = NULL;
gboolean result = FALSE;
GstElement *videoconvert;
sink_pad = gst_bin_find_unlinked_pad (GST_BIN (pipeline->pipeline), GST_PAD_SINK);
if (sink_pad == NULL)
{
g_warning("ShellRecorder: pipeline has no unlinked sink pad");
goto out;
}
pipeline->src = gst_element_factory_make ("shellrecordersrc", NULL);
if (pipeline->src == NULL)
{
g_warning ("Can't create recorder source element");
goto out;
}
gst_bin_add (GST_BIN (pipeline->pipeline), pipeline->src);
recorder_pipeline_set_caps (pipeline);
/* The videoconvert element is a generic converter; it will convert
* our supplied fixed format data into whatever the encoder wants
*/
videoconvert = gst_element_factory_make ("videoconvert", NULL);
if (!videoconvert)
{
g_warning("Can't create videoconvert element");
goto out;
}
gst_bin_add (GST_BIN (pipeline->pipeline), videoconvert);
gst_element_link_many (pipeline->src, videoconvert, NULL);
src_pad = gst_element_get_static_pad (videoconvert, "src");
if (!src_pad)
{
g_warning("ShellRecorder: can't get src pad to link into pipeline");
goto out;
}
if (gst_pad_link (src_pad, sink_pad) != GST_PAD_LINK_OK)
{
g_warning("ShellRecorder: can't link to sink pad");
goto out;
}
result = TRUE;
out:
if (sink_pad)
gst_object_unref (sink_pad);
if (src_pad)
gst_object_unref (src_pad);
return result;
}
static char *
get_absolute_path (char *maybe_relative)
{
char *path;
if (g_path_is_absolute (maybe_relative))
path = g_strdup (maybe_relative);
else
{
const char *video_dir = g_get_user_special_dir (G_USER_DIRECTORY_VIDEOS);
if (!g_file_test (video_dir, G_FILE_TEST_EXISTS))
video_dir = g_get_home_dir ();
path = g_build_filename (video_dir, maybe_relative, NULL);
}
return path;
}
/* Open a file for writing. Opening the file ourselves and using fdsink has
* the advantage over filesink of being able to use O_EXCL when we want to
* avoid overwriting* an existing file. Returns -1 if the file couldn't
* be opened.
*/
static int
recorder_open_outfile (ShellRecorder *recorder,
char **outfilename)
{
const char *pattern;
int flags;
int outfile = -1;
pattern = recorder->file_template;
if (!pattern)
return -1;
while (TRUE)
{
GString *filename = g_string_new (NULL);
const char *p;
char *path;
for (p = pattern; *p; p++)
{
if (*p == '%')
{
switch (*(p + 1))
{
case '%':
case '\0':
g_string_append_c (filename, '%');
break;
case 'd':
{
/* Appends date according to locale */
GDateTime *datetime = g_date_time_new_now_local ();
char *date_str = g_date_time_format (datetime, "%0x");
char *s;
for (s = date_str; *s; s++)
if (G_IS_DIR_SEPARATOR (*s))
*s = '-';
g_string_append (filename, date_str);
g_free (date_str);
g_date_time_unref (datetime);
}
break;
case 't':
{
/* Appends time according to locale */
GDateTime *datetime = g_date_time_new_now_local ();
char *time_str = g_date_time_format (datetime, "%0X");
char *s;
for (s = time_str; *s; s++)
if (G_IS_DIR_SEPARATOR (*s))
*s = ':';
g_string_append (filename, time_str);
g_free (time_str);
g_date_time_unref (datetime);
}
break;
default:
g_warning ("Unknown escape %%%c in filename", *(p + 1));
goto out;
}
p++;
}
else
g_string_append_c (filename, *p);
}
/* If a filename is explicitly specified without %u then we assume the user
* is fine with over-writing the old contents; putting %u in the default
* should avoid problems with malicious symlinks.
*/
flags = O_WRONLY | O_CREAT | O_TRUNC;
path = get_absolute_path (filename->str);
outfile = open (path, flags, 0666);
if (outfile != -1)
{
g_printerr ("Recording to %s\n", path);
if (outfilename != NULL)
*outfilename = path;
else
g_free (path);
g_string_free (filename, TRUE);
goto out;
}
if (outfile == -1 && errno != EEXIST)
{
g_warning ("Cannot open output file '%s': %s", path, g_strerror (errno));
g_string_free (filename, TRUE);
g_free (path);
goto out;
}
g_string_free (filename, TRUE);
g_free (path);
}
out:
return outfile;
}
/* Augments the supplied pipeline with a sink element to write to the output
* file, if necessary.
*/
static gboolean
recorder_pipeline_add_sink (RecorderPipeline *pipeline)
{
GstPad *sink_pad = NULL, *src_pad = NULL;
GstElement *fdsink;
gboolean result = FALSE;
src_pad = gst_bin_find_unlinked_pad (GST_BIN (pipeline->pipeline), GST_PAD_SRC);
if (src_pad == NULL)
{
/* Nothing to do - assume that we were given a complete pipeline */
return TRUE;
}
pipeline->outfile = recorder_open_outfile (pipeline->recorder,
&pipeline->filename);
if (pipeline->outfile == -1)
goto out;
fdsink = gst_element_factory_make ("fdsink", NULL);
if (fdsink == NULL)
{
g_warning("Can't create fdsink element");
goto out;
}
gst_bin_add (GST_BIN (pipeline->pipeline), fdsink);
g_object_set (fdsink, "fd", pipeline->outfile, NULL);
sink_pad = gst_element_get_static_pad (fdsink, "sink");
if (!sink_pad)
{
g_warning("ShellRecorder: can't get sink pad to link pipeline output");
goto out;
}
if (gst_pad_link (src_pad, sink_pad) != GST_PAD_LINK_OK)
{
g_warning("ShellRecorder: can't link to sink pad");
goto out;
}
result = TRUE;
out:
if (src_pad)
gst_object_unref (src_pad);
if (sink_pad)
gst_object_unref (sink_pad);
return result;
}
static gboolean
recorder_update_memory_used_timeout (gpointer data)
{
ShellRecorder *recorder = data;
recorder->update_memory_used_timeout = 0;
recorder_update_memory_used (recorder, TRUE);
return FALSE;
}
/* We throttle down the frequency which we recompute memory usage
* and draw the buffer indicator to avoid cutting into performance.
*/
static void
recorder_pipeline_on_memory_used_changed (ShellRecorderSrc *src,
GParamSpec *spec,
RecorderPipeline *pipeline)
{
ShellRecorder *recorder = pipeline->recorder;
if (!recorder)
return;
if (recorder->update_memory_used_timeout == 0)
{
recorder->update_memory_used_timeout = g_timeout_add (UPDATE_MEMORY_USED_DELAY,
recorder_update_memory_used_timeout,
recorder);
g_source_set_name_by_id (recorder->update_memory_used_timeout, "[gnome-shell] recorder_update_memory_used_timeout");
}
}
static void
recorder_pipeline_free (RecorderPipeline *pipeline)
{
if (pipeline->pipeline != NULL)
gst_object_unref (pipeline->pipeline);
if (pipeline->outfile != -1)
close (pipeline->outfile);
g_free (pipeline->filename);
g_clear_object (&pipeline->recorder);
g_free (pipeline);
}
/* Function gets called on pipeline-global events; we use it to
* know when the pipeline is finished.
*/
static gboolean
recorder_pipeline_bus_watch (GstBus *bus,
GstMessage *message,
gpointer data)
{
RecorderPipeline *pipeline = data;
if (message->type == GST_MESSAGE_EOS)
{
recorder_pipeline_closed (pipeline);
return FALSE; /* remove watch */
}
else if (message->type == GST_MESSAGE_ERROR)
{
GError *error;
gst_message_parse_error (message, &error, NULL);
g_warning ("Error in recording pipeline: %s\n", error->message);
g_error_free (error);
recorder_pipeline_closed (pipeline);
return FALSE; /* remove watch */
}
/* Leave the watch in place */
return TRUE;
}
/* Clean up when the pipeline is finished
*/
static void
recorder_pipeline_closed (RecorderPipeline *pipeline)
{
g_signal_handlers_disconnect_by_func (pipeline->src,
(gpointer) recorder_pipeline_on_memory_used_changed,
pipeline);
recorder_disconnect_stage_callbacks (pipeline->recorder);
gst_element_set_state (pipeline->pipeline, GST_STATE_NULL);
if (pipeline->recorder)
{
GtkRecentManager *recent_manager;
GFile *file;
char *uri;
ShellRecorder *recorder = pipeline->recorder;
if (pipeline == recorder->current_pipeline)
{
/* Error case; force a close */
recorder->current_pipeline = NULL;
shell_recorder_close (recorder);
}
recent_manager = gtk_recent_manager_get_default ();
file = g_file_new_for_path (pipeline->filename);
uri = g_file_get_uri (file);
gtk_recent_manager_add_item (recent_manager,
uri);
g_free (uri);
g_object_unref (file);
recorder->pipelines = g_slist_remove (recorder->pipelines, pipeline);
}
recorder_pipeline_free (pipeline);
}
/*
* Replaces '%T' in the passed pipeline with the thread count,
* the maximum possible value is 64 (limit of what vp9enc supports)
*
* It is assumes that %T occurs only once.
*/
static char*
substitute_thread_count (const char *pipeline)
{
char *tmp;
int n_threads;
GString *result;
tmp = strstr (pipeline, "%T");
if (!tmp)
return g_strdup (pipeline);
#ifdef _SC_NPROCESSORS_ONLN
{
int n_processors = sysconf (_SC_NPROCESSORS_ONLN); /* includes hyper-threading */
n_threads = MIN (MAX (1, n_processors - 1), 64);
}
#else
n_threads = 3;
#endif
result = g_string_new (NULL);
g_string_append_len (result, pipeline, tmp - pipeline);
g_string_append_printf (result, "%d", n_threads);
g_string_append (result, tmp + 2);
return g_string_free (result, FALSE);
}
static gboolean
recorder_open_pipeline (ShellRecorder *recorder)
{
RecorderPipeline *pipeline;
const char *pipeline_description;
char *parsed_pipeline;
GError *error = NULL;
GstBus *bus;
pipeline = g_new0 (RecorderPipeline, 1);
pipeline->recorder = g_object_ref (recorder);
pipeline->outfile = - 1;
pipeline_description = recorder->pipeline_description;
if (!pipeline_description)
pipeline_description = DEFAULT_PIPELINE;
parsed_pipeline = substitute_thread_count (pipeline_description);
pipeline->pipeline = gst_parse_launch_full (parsed_pipeline, NULL,
GST_PARSE_FLAG_FATAL_ERRORS,
&error);
g_free (parsed_pipeline);
if (pipeline->pipeline == NULL)
{
g_warning ("ShellRecorder: failed to parse pipeline: %s", error->message);
g_error_free (error);
goto error;
}
if (!recorder_pipeline_add_source (pipeline))
goto error;
if (!recorder_pipeline_add_sink (pipeline))
goto error;
gst_element_set_state (pipeline->pipeline, GST_STATE_PLAYING);
bus = gst_pipeline_get_bus (GST_PIPELINE (pipeline->pipeline));
gst_bus_add_watch (bus, recorder_pipeline_bus_watch, pipeline);
gst_object_unref (bus);
g_signal_connect (pipeline->src, "notify::memory-used",
G_CALLBACK (recorder_pipeline_on_memory_used_changed), pipeline);
recorder->current_pipeline = pipeline;
recorder->pipelines = g_slist_prepend (recorder->pipelines, pipeline);
return TRUE;
error:
recorder_pipeline_free (pipeline);
return FALSE;
}
static void
recorder_close_pipeline (ShellRecorder *recorder)
{
if (recorder->current_pipeline != NULL)
{
/* This will send an EOS (end-of-stream) message after the last frame
* is written. The bus watch for the pipeline will get it and do
* final cleanup
*/
gst_element_send_event (recorder->current_pipeline->pipeline,
gst_event_new_eos());
recorder->current_pipeline = NULL;
}
}
/**
* shell_recorder_new:
* @stage: The #ClutterStage
*
* Create a new #ShellRecorder to record movies of a #ClutterStage
*
* Return value: The newly created #ShellRecorder object
*/
ShellRecorder *
shell_recorder_new (ClutterStage *stage)
{
return g_object_new (SHELL_TYPE_RECORDER,
"stage", stage,
NULL);
}
/**
* shell_recorder_set_framerate:
* @recorder: the #ShellRecorder
* @framerate: Framerate used for resulting video in frames-per-second.
*
* Sets the number of frames per second we try to record. Less frames
* will be recorded when the screen doesn't need to be redrawn this
* quickly. (This value will also be set as the framerate for the
* GStreamer pipeline; whether that has an effect on the resulting
* video will depend on the details of the pipeline and the codec. The
* default encoding to webm format doesn't pay attention to the pipeline
* framerate.)
*
* The default value is 30.
*/
void
shell_recorder_set_framerate (ShellRecorder *recorder,
int framerate)
{
g_return_if_fail (SHELL_IS_RECORDER (recorder));
recorder_set_framerate (recorder, framerate);
}
/**
* shell_recorder_set_file_template:
* @recorder: the #ShellRecorder
* @file_template: the filename template to use for output files,
* or %NULL for the defalt value.
*
* Sets the filename that will be used when creating output
* files. This is only used if the configured pipeline has an
* unconnected source pad (as the default pipeline does). If
* the pipeline is complete, then the filename is unused. The
* provided string is used as a template.It can contain
* the following escapes:
*
* %d: The current date as YYYYYMMDD
* %%: A literal percent
*
* The default value is 'shell-%d%u-%c.ogg'.
*/
void
shell_recorder_set_file_template (ShellRecorder *recorder,
const char *file_template)
{
g_return_if_fail (SHELL_IS_RECORDER (recorder));
recorder_set_file_template (recorder, file_template);
}
void
shell_recorder_set_draw_cursor (ShellRecorder *recorder,
gboolean draw_cursor)
{
g_return_if_fail (SHELL_IS_RECORDER (recorder));
recorder_set_draw_cursor (recorder, draw_cursor);
}
/**
* shell_recorder_set_pipeline:
* @recorder: the #ShellRecorder
* @pipeline: (nullable): the GStreamer pipeline used to encode recordings
* or %NULL for the default value.
*
* Sets the GStreamer pipeline used to encode recordings.
* It follows the syntax used for gst-launch. The pipeline
* should have an unconnected sink pad where the recorded
* video is recorded. It will normally have a unconnected
* source pad; output from that pad will be written into the
* output file. (See shell_recorder_set_file_template().) However
* the pipeline can also take care of its own output - this
* might be used to send the output to an icecast server
* via shout2send or similar.
*
* The default value is 'vp9enc min_quantizer=13 max_quantizer=13 cpu-used=5 deadline=1000000 threads=%T ! queue ! webmmux'
*/
void
shell_recorder_set_pipeline (ShellRecorder *recorder,
const char *pipeline)
{
g_return_if_fail (SHELL_IS_RECORDER (recorder));
recorder_set_pipeline (recorder, pipeline);
}
void
shell_recorder_set_area (ShellRecorder *recorder,
int x,
int y,
int width,
int height)
{
g_return_if_fail (SHELL_IS_RECORDER (recorder));
recorder->custom_area = TRUE;
recorder->area.x = CLAMP (x, 0, recorder->stage_width);
recorder->area.y = CLAMP (y, 0, recorder->stage_height);
recorder->area.width = CLAMP (width,
0, recorder->stage_width - recorder->area.x);
recorder->area.height = CLAMP (height,
0, recorder->stage_height - recorder->area.y);
/* This breaks the recording but tweaking the GStreamer pipeline a bit
* might make it work, at least if the codec can handle a stream where
* the frame size changes in the middle.
*/
if (recorder->current_pipeline)
recorder_pipeline_set_caps (recorder->current_pipeline);
}
/**
* shell_recorder_record:
* @recorder: the #ShellRecorder
* @filename_used: (out) (optional): actual filename used for recording
*
* Starts recording, Starting the recording may fail if the output file
* cannot be opened, or if the output stream cannot be created
* for other reasons. In that case a warning is printed to
* stderr. There is no way currently to get details on how
* recording failed to start.
*
* An extra reference count is added to the recorder if recording
* is succesfully started; the recording object will not be freed
* until recording is stopped even if the creator no longer holds
* a reference. Recording is automatically stopped if the stage
* is destroyed.
*
* Return value: %TRUE if recording was succesfully started
*/
gboolean
shell_recorder_record (ShellRecorder *recorder,
char **filename_used)
{
g_return_val_if_fail (SHELL_IS_RECORDER (recorder), FALSE);
g_return_val_if_fail (recorder->stage != NULL, FALSE);
g_return_val_if_fail (recorder->state != RECORDER_STATE_RECORDING, FALSE);
if (!recorder_open_pipeline (recorder))
return FALSE;
if (filename_used)
*filename_used = g_strdup (recorder->current_pipeline->filename);
recorder_connect_stage_callbacks (recorder);
recorder->last_frame_time = GST_CLOCK_TIME_NONE;
recorder->state = RECORDER_STATE_RECORDING;
recorder_update_pointer (recorder);
recorder_add_update_pointer_timeout (recorder);
/* Disable unredirection while we are recoring */
meta_disable_unredirect_for_display (shell_global_get_display (shell_global_get ()));
/* Set up repaint hook */
recorder->repaint_hook_id = clutter_threads_add_repaint_func(recorder_repaint_hook, recorder->stage, NULL);
/* Record an initial frame and also redraw with the indicator */
clutter_actor_queue_redraw (CLUTTER_ACTOR (recorder->stage));
/* We keep a ref while recording to let a caller start a recording then
* drop their reference to the recorder
*/
g_object_ref (recorder);
return TRUE;
}
/**
* shell_recorder_close:
* @recorder: the #ShellRecorder
*
* Stops recording. It's possible to call shell_recorder_record()
* again to reopen a new recording stream, but unless change the
* recording filename, this may result in the old recording being
* overwritten.
*/
void
shell_recorder_close (ShellRecorder *recorder)
{
g_return_if_fail (SHELL_IS_RECORDER (recorder));
g_return_if_fail (recorder->state != RECORDER_STATE_CLOSED);
/* We want to record one more frame since some time may have
* elapsed since the last frame
*/
recorder_record_frame (recorder, TRUE);
recorder_remove_update_pointer_timeout (recorder);
recorder_close_pipeline (recorder);
/* Queue a redraw to remove the recording indicator */
clutter_actor_queue_redraw (CLUTTER_ACTOR (recorder->stage));
if (recorder->repaint_hook_id != 0)
{
clutter_threads_remove_repaint_func (recorder->repaint_hook_id);
recorder->repaint_hook_id = 0;
}
recorder->state = RECORDER_STATE_CLOSED;
/* Reenable after the recording */
meta_enable_unredirect_for_display (shell_global_get_display (shell_global_get ()));
/* Release the refcount we took when we started recording */
g_object_unref (recorder);
}
/**
* shell_recorder_is_recording:
*
* Determine if recording is currently in progress. (The recorder
* is not paused or closed.)
*
* Return value: %TRUE if the recorder is currently recording.
*/
gboolean
shell_recorder_is_recording (ShellRecorder *recorder)
{
g_return_val_if_fail (SHELL_IS_RECORDER (recorder), FALSE);
return recorder->state == RECORDER_STATE_RECORDING;
}