248a0c1b6c
It looks a bit unpolished to overlap our own chrome with the recording icon, which may happen when an existing adds UI at the bottom edge. Fix this by using the primary monitor's workarea for the position rather than the entire monitor. https://bugzilla.gnome.org/show_bug.cgi?id=700409
1893 lines
57 KiB
C
1893 lines
57 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 "shell-recorder-src.h"
|
|
#include "shell-recorder.h"
|
|
|
|
#include <clutter/x11/clutter-x11.h>
|
|
#include <X11/extensions/Xfixes.h>
|
|
#include <X11/extensions/XInput.h>
|
|
#include <X11/extensions/XInput2.h>
|
|
|
|
/* This is also hard-coded in mutter and GDK */
|
|
#define VIRTUAL_CORE_POINTER_ID 2
|
|
|
|
typedef enum {
|
|
RECORDER_STATE_CLOSED,
|
|
RECORDER_STATE_RECORDING
|
|
} RecorderState;
|
|
|
|
typedef struct _RecorderPipeline RecorderPipeline;
|
|
|
|
struct _ShellRecorderClass
|
|
{
|
|
GObjectClass parent_class;
|
|
};
|
|
|
|
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;
|
|
|
|
gboolean have_pointer;
|
|
int pointer_x;
|
|
int pointer_y;
|
|
|
|
gboolean have_xfixes;
|
|
int xfixes_event_base;
|
|
|
|
int xinput_opcode;
|
|
|
|
CoglHandle recording_icon; /* icon shown while playing */
|
|
|
|
gboolean draw_cursor;
|
|
cairo_surface_t *cursor_image;
|
|
int cursor_hot_x;
|
|
int cursor_hot_y;
|
|
|
|
gboolean only_paint; /* Used to temporarily suppress recording */
|
|
|
|
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 start_time; /* When we started recording */
|
|
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);
|
|
|
|
enum {
|
|
PROP_0,
|
|
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. videorate is used to give a constant stream of
|
|
* frames to theora even if there is a pause because nothing is moving.
|
|
* (Theora does have some support for frames at non-uniform times, but
|
|
* things seem to break down if there are large gaps.)
|
|
*/
|
|
#define DEFAULT_PIPELINE "vp8enc 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)
|
|
|
|
/* Create an emblem to show at the lower-left corner of the stage while
|
|
* recording. The emblem is drawn *after* we record the frame so doesn't
|
|
* show up in the frame.
|
|
*/
|
|
static CoglHandle
|
|
create_recording_icon (void)
|
|
{
|
|
cairo_surface_t *surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, 32, 32);
|
|
cairo_t *cr;
|
|
cairo_pattern_t *pat;
|
|
CoglHandle texture;
|
|
|
|
cr = cairo_create (surface);
|
|
|
|
/* clear to transparent */
|
|
cairo_save (cr);
|
|
cairo_set_operator (cr, CAIRO_OPERATOR_CLEAR);
|
|
cairo_paint (cr);
|
|
cairo_restore (cr);
|
|
|
|
/* radial "glow" */
|
|
pat = cairo_pattern_create_radial (16, 16, 6,
|
|
16, 16, 14);
|
|
cairo_pattern_add_color_stop_rgba (pat, 0.0,
|
|
1, 0, 0, 1); /* opaque red */
|
|
cairo_pattern_add_color_stop_rgba (pat, 1.0,
|
|
1, 0, 0, 0); /* transparent red */
|
|
|
|
cairo_set_source (cr, pat);
|
|
cairo_paint (cr);
|
|
cairo_pattern_destroy (pat);
|
|
|
|
/* red circle */
|
|
cairo_arc (cr, 16, 16, 8,
|
|
0, 2 * M_PI);
|
|
cairo_set_source_rgb (cr, 1, 0, 0);
|
|
cairo_fill (cr);
|
|
|
|
cairo_destroy (cr);
|
|
|
|
texture = cogl_texture_new_from_data (32, 32,
|
|
COGL_TEXTURE_NONE,
|
|
CLUTTER_CAIRO_FORMAT_ARGB32,
|
|
COGL_PIXEL_FORMAT_ANY,
|
|
cairo_image_surface_get_stride (surface),
|
|
cairo_image_surface_get_data (surface));
|
|
cairo_surface_destroy (surface);
|
|
|
|
return texture;
|
|
}
|
|
|
|
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->recording_icon = create_recording_icon ();
|
|
recorder->memory_target = get_memory_target();
|
|
|
|
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);
|
|
|
|
recorder_set_stage (recorder, NULL);
|
|
recorder_set_pipeline (recorder, NULL);
|
|
recorder_set_file_template (recorder, NULL);
|
|
|
|
cogl_handle_unref (recorder->recording_icon);
|
|
|
|
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;
|
|
if (repaint)
|
|
{
|
|
/* In other cases we just queue a redraw even if we only need
|
|
* to repaint and not redraw a frame, but having changes in
|
|
* memory usage cause frames to be painted and memory used
|
|
* seems like a bad idea.
|
|
*/
|
|
recorder->only_paint = TRUE;
|
|
clutter_stage_ensure_redraw (recorder->stage);
|
|
recorder->only_paint = FALSE;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* 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);
|
|
}
|
|
}
|
|
|
|
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)
|
|
{
|
|
XFixesCursorImage *cursor_image;
|
|
guchar *data;
|
|
int stride;
|
|
int i, j;
|
|
|
|
if (!recorder->have_xfixes)
|
|
return;
|
|
|
|
cursor_image = XFixesGetCursorImage (clutter_x11_get_default_display ());
|
|
if (!cursor_image)
|
|
return;
|
|
|
|
recorder->cursor_hot_x = cursor_image->xhot;
|
|
recorder->cursor_hot_y = cursor_image->yhot;
|
|
|
|
recorder->cursor_image = cairo_image_surface_create (CAIRO_FORMAT_ARGB32,
|
|
cursor_image->width,
|
|
cursor_image->height);
|
|
|
|
/* The pixel data (in typical Xlib breakage) is longs even on
|
|
* 64-bit platforms, so we have to data-convert there. For simplicity,
|
|
* just do it always
|
|
*/
|
|
data = cairo_image_surface_get_data (recorder->cursor_image);
|
|
stride = cairo_image_surface_get_stride (recorder->cursor_image);
|
|
for (i = 0; i < cursor_image->height; i++)
|
|
for (j = 0; j < cursor_image->width; j++)
|
|
*(guint32 *)(data + i * stride + 4 * j) = cursor_image->pixels[i * cursor_image->width + j];
|
|
|
|
cairo_surface_mark_dirty (recorder->cursor_image);
|
|
|
|
XFree (cursor_image);
|
|
}
|
|
|
|
/* 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);
|
|
}
|
|
|
|
/* Draw an overlay indicating how much of the target memory is used
|
|
* for buffering frames.
|
|
*/
|
|
static void
|
|
recorder_draw_buffer_meter (ShellRecorder *recorder)
|
|
{
|
|
int fill_level;
|
|
GdkRectangle primary_monitor;
|
|
float rects[16];
|
|
|
|
gdk_screen_get_monitor_workarea (recorder->gdk_screen,
|
|
gdk_screen_get_primary_monitor (recorder->gdk_screen),
|
|
&primary_monitor);
|
|
|
|
recorder_update_memory_used (recorder, FALSE);
|
|
|
|
/* As the buffer gets more full, we go from green, to yellow, to red */
|
|
if (recorder->memory_used > (recorder->memory_target * 3) / 4)
|
|
cogl_set_source_color4f (1, 0, 0, 1);
|
|
else if (recorder->memory_used > recorder->memory_target / 2)
|
|
cogl_set_source_color4f (1, 1, 0, 1);
|
|
else
|
|
cogl_set_source_color4f (0, 1, 0, 1);
|
|
|
|
fill_level = MIN (60, (recorder->memory_used * 60) / recorder->memory_target);
|
|
|
|
/* A hollow rectangle filled from the left to fill_level */
|
|
rects[0] = primary_monitor.x + primary_monitor.width - 64;
|
|
rects[1] = primary_monitor.y + primary_monitor.height - 10;
|
|
rects[2] = primary_monitor.x + primary_monitor.width - 2;
|
|
rects[3] = primary_monitor.y + primary_monitor.height - 9;
|
|
|
|
rects[4] = primary_monitor.x + primary_monitor.width - 64;
|
|
rects[5] = primary_monitor.y + primary_monitor.height - 9;
|
|
rects[6] = primary_monitor.x + primary_monitor.width - (63 - fill_level);
|
|
rects[7] = primary_monitor.y + primary_monitor.height - 3;
|
|
|
|
rects[8] = primary_monitor.x + primary_monitor.width - 3;
|
|
rects[9] = primary_monitor.y + primary_monitor.height - 9;
|
|
rects[10] = primary_monitor.x + primary_monitor.width - 2;
|
|
rects[11] = primary_monitor.y + primary_monitor.height - 3;
|
|
|
|
rects[12] = primary_monitor.x + primary_monitor.width - 64;
|
|
rects[13] = primary_monitor.y + primary_monitor.height - 3;
|
|
rects[14] = primary_monitor.x + primary_monitor.width - 2;
|
|
rects[15] = primary_monitor.y + primary_monitor.height - 2;
|
|
|
|
cogl_rectangles (rects, 4);
|
|
}
|
|
|
|
/* We want to time-stamp each frame based on the actual time it was
|
|
* recorded. We probably should use the pipeline clock rather than
|
|
* gettimeofday(): that would be needed to get sync'ed audio correct.
|
|
* I'm not immediately sure how to handle the adjustment we currently
|
|
* do when pausing recording - is pausing the pipeline enough?
|
|
*/
|
|
static GstClockTime
|
|
get_wall_time (void)
|
|
{
|
|
GTimeVal tv;
|
|
|
|
g_get_current_time (&tv);
|
|
|
|
return tv.tv_sec * 1000000000LL + tv.tv_usec * 1000LL;
|
|
}
|
|
|
|
/* Retrieve a frame and feed it into the pipeline
|
|
*/
|
|
static void
|
|
recorder_record_frame (ShellRecorder *recorder)
|
|
{
|
|
GstBuffer *buffer;
|
|
guint8 *data;
|
|
guint size;
|
|
GstClockTime now;
|
|
|
|
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.
|
|
*/
|
|
now = get_wall_time();
|
|
if (now - recorder->last_frame_time < (3 * 1000000000LL / (4 * recorder->framerate)))
|
|
return;
|
|
|
|
recorder->last_frame_time = now;
|
|
|
|
size = recorder->area.width * recorder->area.height * 4;
|
|
|
|
data = g_malloc (recorder->area.width * 4 * recorder->area.height);
|
|
cogl_read_pixels (recorder->area.x,
|
|
recorder->area.y,
|
|
recorder->area.width,
|
|
recorder->area.height,
|
|
COGL_READ_PIXELS_COLOR_BUFFER,
|
|
CLUTTER_CAIRO_FORMAT_ARGB32,
|
|
data);
|
|
|
|
buffer = gst_buffer_new();
|
|
gst_buffer_insert_memory (buffer, -1,
|
|
gst_memory_new_wrapped (0, data, size, 0,
|
|
size, data, g_free));
|
|
|
|
GST_BUFFER_PTS(buffer) = now - recorder->start_time;
|
|
|
|
if (recorder->draw_cursor)
|
|
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)
|
|
{
|
|
GdkRectangle primary_monitor;
|
|
|
|
gdk_screen_get_monitor_workarea (recorder->gdk_screen,
|
|
gdk_screen_get_primary_monitor (recorder->gdk_screen),
|
|
&primary_monitor);
|
|
if (!recorder->only_paint)
|
|
recorder_record_frame (recorder);
|
|
|
|
cogl_set_source_texture (recorder->recording_icon);
|
|
cogl_rectangle (primary_monitor.x + primary_monitor.width - 32, primary_monitor.y + primary_monitor.height - 42,
|
|
primary_monitor.x + primary_monitor.width, primary_monitor.y + primary_monitor.height - 10);
|
|
}
|
|
|
|
if (recorder->state == RECORDER_STATE_RECORDING || recorder->memory_used != 0)
|
|
recorder_draw_buffer_meter (recorder);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
/* We use an event filter on the stage to get the XFixesCursorNotifyEvent
|
|
* and also to track cursor position (when the cursor is over the stage's
|
|
* input area); tracking cursor position here rather than with ClutterEvent
|
|
* allows us to avoid worrying about event propagation and competing
|
|
* signal handlers.
|
|
*/
|
|
static ClutterX11FilterReturn
|
|
recorder_event_filter (XEvent *xev,
|
|
ClutterEvent *cev,
|
|
gpointer data)
|
|
{
|
|
ShellRecorder *recorder = data;
|
|
XIEvent *input_event = NULL;
|
|
|
|
if (xev->xany.window != clutter_x11_get_stage_window (recorder->stage))
|
|
return CLUTTER_X11_FILTER_CONTINUE;
|
|
|
|
if (xev->xany.type == GenericEvent &&
|
|
xev->xcookie.extension == recorder->xinput_opcode)
|
|
input_event = (XIEvent *) xev->xcookie.data;
|
|
|
|
if (xev->xany.type == recorder->xfixes_event_base + XFixesCursorNotify)
|
|
{
|
|
XFixesCursorNotifyEvent *notify_event = (XFixesCursorNotifyEvent *)xev;
|
|
|
|
if (notify_event->subtype == XFixesDisplayCursorNotify)
|
|
{
|
|
if (recorder->cursor_image)
|
|
{
|
|
cairo_surface_destroy (recorder->cursor_image);
|
|
recorder->cursor_image = NULL;
|
|
}
|
|
|
|
recorder_queue_redraw (recorder);
|
|
}
|
|
}
|
|
else if (input_event != NULL &&
|
|
input_event->evtype == XI_Motion)
|
|
{
|
|
XIDeviceEvent *device_event = (XIDeviceEvent *) input_event;
|
|
if (device_event->deviceid == VIRTUAL_CORE_POINTER_ID)
|
|
{
|
|
recorder->pointer_x = device_event->event_x;
|
|
recorder->pointer_y = device_event->event_y;
|
|
|
|
recorder_queue_redraw (recorder);
|
|
}
|
|
}
|
|
/* We want to track whether the pointer is over the stage
|
|
* window itself, and not in a child window. A "virtual"
|
|
* crossing is one that goes directly from ancestor to child.
|
|
*/
|
|
else if (input_event != NULL &&
|
|
input_event->evtype == XI_Enter)
|
|
{
|
|
XIEnterEvent *enter_event = (XIEnterEvent *) input_event;
|
|
|
|
if (enter_event->deviceid == VIRTUAL_CORE_POINTER_ID &&
|
|
(enter_event->detail != XINotifyVirtual &&
|
|
enter_event->detail != XINotifyNonlinearVirtual))
|
|
{
|
|
recorder->have_pointer = TRUE;
|
|
recorder->pointer_x = enter_event->event_x;
|
|
recorder->pointer_y = enter_event->event_y;
|
|
|
|
recorder_queue_redraw (recorder);
|
|
}
|
|
}
|
|
else if (input_event != NULL &&
|
|
input_event->evtype == XI_Leave)
|
|
{
|
|
XILeaveEvent *leave_event = (XILeaveEvent *) input_event;
|
|
|
|
if (leave_event->deviceid == VIRTUAL_CORE_POINTER_ID &&
|
|
(leave_event->detail != XINotifyVirtual &&
|
|
leave_event->detail != XINotifyNonlinearVirtual))
|
|
{
|
|
recorder->have_pointer = FALSE;
|
|
recorder->pointer_x = leave_event->event_x;
|
|
recorder->pointer_y = leave_event->event_y;
|
|
|
|
recorder_queue_redraw (recorder);
|
|
}
|
|
}
|
|
|
|
return CLUTTER_X11_FILTER_CONTINUE;
|
|
}
|
|
|
|
/* We optimize out querying the server for the pointer position if the
|
|
* pointer is in the input area of the ClutterStage. We track changes to
|
|
* that with Enter/Leave events, but we need to 100% accurate about the
|
|
* initial condition, which is a little involved.
|
|
*/
|
|
static void
|
|
recorder_get_initial_cursor_position (ShellRecorder *recorder)
|
|
{
|
|
Display *xdisplay = clutter_x11_get_default_display ();
|
|
Window xwindow = clutter_x11_get_stage_window (recorder->stage);
|
|
XWindowAttributes xwa;
|
|
Window root, child, parent;
|
|
Window *children;
|
|
guint n_children;
|
|
int root_x,root_y;
|
|
int window_x, window_y;
|
|
guint mask;
|
|
|
|
XGrabServer(xdisplay);
|
|
|
|
XGetWindowAttributes (xdisplay, xwindow, &xwa);
|
|
XQueryTree (xdisplay, xwindow, &root, &parent, &children, &n_children);
|
|
XFree (children);
|
|
|
|
if (xwa.map_state == IsViewable &&
|
|
XQueryPointer (xdisplay, parent,
|
|
&root, &child, &root_x, &root_y, &window_x, &window_y, &mask) &&
|
|
child == xwindow)
|
|
{
|
|
/* The point of this call is not actually to translate the coordinates -
|
|
* we could do that ourselves using xwa.{x,y} - but rather to see if
|
|
* the pointer is in a child of the window, which we count as "not in
|
|
* window", because we aren't guaranteed to get pointer events.
|
|
*/
|
|
XTranslateCoordinates(xdisplay, parent, xwindow,
|
|
window_x, window_y,
|
|
&window_x, &window_y, &child);
|
|
if (child == None)
|
|
{
|
|
recorder->have_pointer = TRUE;
|
|
recorder->pointer_x = window_x;
|
|
recorder->pointer_y = window_y;
|
|
}
|
|
}
|
|
else
|
|
recorder->have_pointer = FALSE;
|
|
|
|
XUngrabServer(xdisplay);
|
|
XFlush(xdisplay);
|
|
}
|
|
|
|
/* When the cursor is not over the stage's input area, we query for the
|
|
* pointer position in a timeout.
|
|
*/
|
|
static void
|
|
recorder_update_pointer (ShellRecorder *recorder)
|
|
{
|
|
Display *xdisplay = clutter_x11_get_default_display ();
|
|
Window xwindow = clutter_x11_get_stage_window (recorder->stage);
|
|
Window root, child;
|
|
int root_x,root_y;
|
|
int window_x, window_y;
|
|
guint mask;
|
|
|
|
if (recorder->have_pointer)
|
|
return;
|
|
|
|
if (XQueryPointer (xdisplay, xwindow,
|
|
&root, &child, &root_x, &root_y, &window_x, &window_y, &mask))
|
|
{
|
|
if (window_x != recorder->pointer_x || window_y != recorder->pointer_y)
|
|
{
|
|
recorder->pointer_x = window_x;
|
|
recorder->pointer_y = window_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);
|
|
}
|
|
|
|
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::width",
|
|
G_CALLBACK (recorder_on_stage_notify_size), recorder);
|
|
|
|
clutter_x11_add_filter (recorder_event_filter, 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);
|
|
|
|
clutter_x11_remove_filter (recorder_event_filter, 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)
|
|
{
|
|
int error_base, event_base;
|
|
int major = 2, minor = 3;
|
|
|
|
recorder->stage = stage;
|
|
|
|
recorder_update_size (recorder);
|
|
|
|
recorder->have_xfixes = XFixesQueryExtension (clutter_x11_get_default_display (),
|
|
&recorder->xfixes_event_base,
|
|
&error_base);
|
|
if (recorder->have_xfixes)
|
|
XFixesSelectCursorInput (clutter_x11_get_default_display (),
|
|
clutter_x11_get_stage_window (stage),
|
|
XFixesDisplayCursorNotifyMask);
|
|
|
|
if (XQueryExtension (clutter_x11_get_default_display (),
|
|
"XInputExtension",
|
|
&recorder->xinput_opcode,
|
|
&error_base,
|
|
&event_base))
|
|
{
|
|
if (XIQueryVersion (clutter_x11_get_default_display (), &major, &minor) == Success)
|
|
{
|
|
int version = (major * 10) + minor;
|
|
if (version < 22)
|
|
g_warning("ShellRecorder: xinput version %d.%d is too old", major, minor);
|
|
}
|
|
else
|
|
{
|
|
g_warning("ShellRecorder: xinput version could not be queried");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
g_warning("ShellRecorder: xinput extension unavailable");
|
|
}
|
|
|
|
clutter_stage_ensure_current (stage);
|
|
|
|
recorder_get_initial_cursor_position (recorder);
|
|
}
|
|
}
|
|
|
|
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_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_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
|
|
"bpp", G_TYPE_INT, 32,
|
|
"depth", G_TYPE_INT, 24,
|
|
"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);
|
|
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", filename->str, 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);
|
|
}
|
|
|
|
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;
|
|
|
|
switch (message->type)
|
|
{
|
|
case GST_MESSAGE_EOS:
|
|
recorder_pipeline_closed (pipeline);
|
|
return FALSE; /* remove watch */
|
|
case 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 */
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
|
|
/* 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 vp8enc 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
|
|
*/
|
|
shell_recorder_src_close (SHELL_RECORDER_SRC (recorder->current_pipeline->src));
|
|
|
|
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: (allow-none): 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 'vp8enc 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) (allow-none): 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->start_time = get_wall_time();
|
|
recorder->last_frame_time = 0;
|
|
|
|
recorder->state = RECORDER_STATE_RECORDING;
|
|
recorder_add_update_pointer_timeout (recorder);
|
|
|
|
/* 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
|
|
*/
|
|
clutter_actor_paint (CLUTTER_ACTOR (recorder->stage));
|
|
|
|
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;
|
|
|
|
/* 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;
|
|
}
|