[gnome-shell] Add a built-in screencast recording facility
- From: Owen Taylor <otaylor src gnome org>
- To: svn-commits-list gnome org
- Subject: [gnome-shell] Add a built-in screencast recording facility
- Date: Fri, 20 Mar 2009 16:56:26 -0400 (EDT)
commit afceea3fe6ecf4096a33314d40240e743171f6ef
Author: Owen W. Taylor <otaylor redhat com>
Date: Fri Mar 13 17:14:31 2009 -0400
Add a built-in screencast recording facility
For development and demonstration purposes, it's neat to be able to
record a screencast of gnome-shell without any external setup.
Built-in recording can also give much better quality than is possible
with a generic desktop recording, since we hook right into the paint
loop.
src/shell-recorder.[ch]: A general-purposes object to record a Clutter
stage to a GStreamer stream.
src/shell-recorder-src.[ch]: A simple GStreamer source element (similar
to appsrc in the most recent versions of GStreamer) for injecting
captured data into a GStreamer pipeline.
src/test-recorder.c: Test program that records a simple animation.
configure.ac src/Makefile.am: Add machinery to conditionally build
ShellRecorder.
tools/build/gnome-shell-build-setup.sh: Add gstreamer packages
to the list of required packages for Fedora.
js/ui/main.js: Hook up the recorder to a MetaScreen ::toggle-recording
keybinding.
http://bugzilla.gnome.org/show_bug.cgi?id=575290
---
.gitignore | 2 +
configure.ac | 22 +-
js/ui/main.js | 19 +
src/Makefile.am | 26 +
src/shell-recorder-src.c | 298 ++++++
src/shell-recorder-src.h | 39 +
src/shell-recorder.c | 1666 ++++++++++++++++++++++++++++++++
src/shell-recorder.h | 43 +
src/test-recorder.c | 95 ++
tools/build/gnome-shell-build-setup.sh | 1 +
10 files changed, 2210 insertions(+), 1 deletions(-)
diff --git a/.gitignore b/.gitignore
index 4d4c17e..a57399e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,4 +26,6 @@ src/Makefile
src/Makefile.in
src/gnomeshell-taskpanel
src/gnome-shell
+src/test-recorder
+src/test-recorder.ogg
stamp-h1
diff --git a/configure.ac b/configure.ac
index 756fb4f..410e993 100644
--- a/configure.ac
+++ b/configure.ac
@@ -18,7 +18,27 @@ AC_SUBST(GETTEXT_PACKAGE)
AC_DEFINE_UNQUOTED(GETTEXT_PACKAGE, "$GETTEXT_PACKAGE",
[The prefix for our gettext translation domains.])
-PKG_CHECK_MODULES(MUTTER_PLUGIN, gtk+-2.0 dbus-glib-1 metacity-plugins gjs-gi-1.0)
+PKG_PROG_PKG_CONFIG(0.16)
+
+# We need at least this, since gst_plugin_register_static() was added
+# in 0.10.16, but nothing older than 0.10.21 has been tested.
+GSTREAMER_MIN_VERSION=0.10.16
+
+recorder_modules=
+build_recorder=false
+AC_MSG_CHECKING([for GStreamer (needed for recording functionality)])
+if $PKG_CONFIG --exists gstreamer-0.10 '>=' $GSTREAMER_MIN_VERSION ; then
+ AC_MSG_RESULT(yes)
+ build_recorder=true
+ recorder_modules="gstreamer-0.10 gstreamer-base-0.10 xfixes"
+ PKG_CHECK_MODULES(TEST_SHELL_RECORDER, $recorder_modules clutter-0.9)
+else
+ AC_MSG_RESULT(no)
+fi
+
+AM_CONDITIONAL(BUILD_RECORDER, $build_recorder)
+
+PKG_CHECK_MODULES(MUTTER_PLUGIN, gtk+-2.0 dbus-glib-1 metacity-plugins gjs-gi-1.0 $recorder_modules)
PKG_CHECK_MODULES(TIDY, clutter-0.9)
PKG_CHECK_MODULES(BIG, clutter-0.9 gtk+-2.0 librsvg-2.0)
PKG_CHECK_MODULES(GDMUSER, dbus-glib-1 gtk+-2.0)
diff --git a/js/ui/main.js b/js/ui/main.js
index 0b02519..449553c 100644
--- a/js/ui/main.js
+++ b/js/ui/main.js
@@ -20,6 +20,7 @@ let overlay = null;
let overlayActive = false;
let runDialog = null;
let wm = null;
+let recorder = null;
function start() {
let global = Shell.Global.get();
@@ -71,6 +72,24 @@ function start() {
show_overlay();
}
};
+
+ global.screen.connect('toggle-recording', function() {
+ if (recorder == null) {
+ // We have to initialize GStreamer first. This isn't done
+ // inside ShellRecorder to make it usable inside projects
+ // with other usage of GStreamer.
+ let Gst = imports.gi.Gst;
+ Gst.init(null, null);
+ recorder = new Shell.Recorder({ stage: global.stage });
+ }
+
+ if (recorder.is_recording()) {
+ recorder.pause();
+ } else {
+ recorder.record();
+ }
+ });
+
display.connect('overlay-key', toggleOverlay);
global.connect('panel-main-menu', toggleOverlay);
diff --git a/src/Makefile.am b/src/Makefile.am
index cc29a83..9b2da57 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -66,6 +66,32 @@ libgnome_shell_la_SOURCES = \
# ClutterGLXTexturePixmap is currently not wrapped
non_gir_sources = shell-gtkwindow-actor.h
+
+shell_recorder_sources = \
+ shell-recorder.c \
+ shell-recorder.h \
+ shell-recorder-src.c \
+ shell-recorder-src.h
+
+# Custom element is an internal detail
+shell_recorder_non_gir_sources = \
+ shell-recorder-src.c \
+ shell-recorder-src.h
+
+if BUILD_RECORDER
+libgnome_shell_la_SOURCES += $(shell_recorder_sources)
+non_gir_sources += $(shell_recorder_non_gir_sources)
+
+noinst_PROGRAMS = test-recorder
+
+test_recorder_CPPFLAGS = $(TEST_SHELL_RECORDER_CFLAGS)
+test_recorder_LDADD = $(TEST_SHELL_RECORDER_LIBS)
+
+test_recorder_SOURCES = \
+ $(shell_recorder_sources) \
+ test-recorder.c
+endif BUILD_RECORDER
+
libgnome_shell_la_gir_sources = \
$(filter-out $(non_gir_sources), $(libgnome_shell_la_SOURCES))
diff --git a/src/shell-recorder-src.c b/src/shell-recorder-src.c
new file mode 100644
index 0000000..9bce4d7
--- /dev/null
+++ b/src/shell-recorder-src.c
@@ -0,0 +1,298 @@
+#include <gst/base/gstpushsrc.h>
+
+#include "shell-recorder-src.h"
+
+struct _ShellRecorderSrc
+{
+ GstPushSrc parent;
+
+ GMutex *mutex;
+
+ GstCaps *caps;
+ GAsyncQueue *queue;
+ gboolean closed;
+ guint memory_used;
+ guint memory_used_update_idle;
+};
+
+struct _ShellRecorderSrcClass
+{
+ GstPushSrcClass parent_class;
+};
+
+enum {
+ PROP_0,
+ PROP_CAPS,
+ PROP_MEMORY_USED
+};
+
+/* Special marker value once the source is closed */
+#define RECORDER_QUEUE_END ((GstBuffer *)1)
+
+GST_BOILERPLATE(ShellRecorderSrc, shell_recorder_src, GstPushSrc, GST_TYPE_PUSH_SRC);
+
+static void
+shell_recorder_src_init (ShellRecorderSrc *src,
+ ShellRecorderSrcClass *klass)
+{
+ src->queue = g_async_queue_new ();
+ src->mutex = g_mutex_new ();
+}
+
+static void
+shell_recorder_src_base_init (gpointer klass)
+{
+}
+
+static gboolean
+shell_recorder_src_memory_used_update_idle (gpointer data)
+{
+ ShellRecorderSrc *src = data;
+
+ g_mutex_lock (src->mutex);
+ src->memory_used_update_idle = 0;
+ g_mutex_unlock (src->mutex);
+
+ g_object_notify (G_OBJECT (src), "memory-used");
+
+ return FALSE;
+}
+
+/* The memory_used property is used to monitor buffer usage,
+ * so we marshal notification back to the main loop thread.
+ */
+static void
+shell_recorder_src_update_memory_used (ShellRecorderSrc *src,
+ int delta)
+{
+ g_mutex_lock (src->mutex);
+ src->memory_used += delta;
+ if (src->memory_used_update_idle == 0)
+ src->memory_used_update_idle = g_idle_add (shell_recorder_src_memory_used_update_idle, src);
+ g_mutex_unlock (src->mutex);
+}
+
+/* The create() virtual function is responsible for returning the next buffer.
+ * We just pop buffers off of the queue and block if necessary.
+ */
+static GstFlowReturn
+shell_recorder_src_create (GstPushSrc *push_src,
+ GstBuffer **buffer_out)
+{
+ ShellRecorderSrc *src = SHELL_RECORDER_SRC (push_src);
+ GstBuffer *buffer;
+
+ if (src->closed)
+ return GST_FLOW_UNEXPECTED;
+
+ buffer = g_async_queue_pop (src->queue);
+ if (buffer == RECORDER_QUEUE_END)
+ {
+ /* Returning UNEXPECTED here will cause a EOS message to be sent */
+ src->closed = TRUE;
+ return GST_FLOW_UNEXPECTED;
+ }
+
+ shell_recorder_src_update_memory_used (src,
+ - (int)(GST_BUFFER_SIZE(buffer) / 1024));
+
+ *buffer_out = buffer;
+
+ return GST_FLOW_OK;
+}
+
+static void
+shell_recorder_src_set_caps (ShellRecorderSrc *src,
+ const GstCaps *caps)
+{
+ if (caps == src->caps)
+ return;
+
+ if (src->caps != NULL)
+ {
+ gst_caps_unref (src->caps);
+ src->caps = NULL;
+ }
+
+ if (caps)
+ {
+ /* The capabilities will be negotated with the downstream element
+ * and set on the pad when the first buffer is pushed.
+ */
+ src->caps = gst_caps_copy (caps);
+ }
+ else
+ src->caps = NULL;
+}
+
+static void
+shell_recorder_src_finalize (GObject *object)
+{
+ ShellRecorderSrc *src = SHELL_RECORDER_SRC (object);
+
+ if (src->memory_used_update_idle)
+ g_source_remove (src->memory_used_update_idle);
+
+ shell_recorder_src_set_caps (src, NULL);
+ g_async_queue_unref (src->queue);
+
+ g_mutex_free (src->mutex);
+
+ G_OBJECT_CLASS (parent_class)->finalize (object);
+}
+
+static void
+shell_recorder_src_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ ShellRecorderSrc *src = SHELL_RECORDER_SRC (object);
+
+ switch (prop_id)
+ {
+ case PROP_CAPS:
+ shell_recorder_src_set_caps (src, gst_value_get_caps (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+shell_recorder_src_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ ShellRecorderSrc *src = SHELL_RECORDER_SRC (object);
+
+ switch (prop_id)
+ {
+ case PROP_CAPS:
+ gst_value_set_caps (value, src->caps);
+ break;
+ case PROP_MEMORY_USED:
+ g_mutex_lock (src->mutex);
+ g_value_set_uint (value, src->memory_used);
+ g_mutex_unlock (src->mutex);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+shell_recorder_src_class_init (ShellRecorderSrcClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GstElementClass *element_class = GST_ELEMENT_CLASS (klass);
+ GstPushSrcClass *push_src_class = GST_PUSH_SRC_CLASS (klass);
+
+ static GstStaticPadTemplate src_template =
+ GST_STATIC_PAD_TEMPLATE ("src",
+ GST_PAD_SRC,
+ GST_PAD_ALWAYS,
+ GST_STATIC_CAPS_ANY);
+
+ object_class->finalize = shell_recorder_src_finalize;
+ object_class->set_property = shell_recorder_src_set_property;
+ object_class->get_property = shell_recorder_src_get_property;
+
+ push_src_class->create = shell_recorder_src_create;
+
+ g_object_class_install_property (object_class,
+ PROP_CAPS,
+ g_param_spec_boxed ("caps",
+ "Caps",
+ "Fixed GstCaps for the source",
+ GST_TYPE_CAPS,
+ G_PARAM_READWRITE));
+ g_object_class_install_property (object_class,
+ PROP_MEMORY_USED,
+ g_param_spec_uint ("memory-used",
+ "Memory Used",
+ "Memory currently used by the queue (in kB)",
+ 0, G_MAXUINT, 0,
+ G_PARAM_READABLE));
+ gst_element_class_add_pad_template (element_class,
+ gst_static_pad_template_get (&src_template));
+
+ gst_element_class_set_details_simple (element_class,
+ "ShellRecorderSrc",
+ "Generic/Src",
+ "Feed screen capture data to a pipeline",
+ "Owen Taylor <otaylor redhat com>");
+}
+
+/**
+ * shell_recorder_src_add_buffer:
+ *
+ * Adds a buffer to the internal queue to be pushed out at the next opportunity.
+ * There is no flow control, so arbitrary amounts of memory may be used by
+ * the buffers on the queue. The buffer contents must match the #GstCaps
+ * set in the :caps property.
+ */
+void
+shell_recorder_src_add_buffer (ShellRecorderSrc *src,
+ GstBuffer *buffer)
+{
+ g_return_if_fail (SHELL_IS_RECORDER_SRC (src));
+ g_return_if_fail (src->caps != NULL);
+
+ gst_buffer_set_caps (buffer, src->caps);
+ shell_recorder_src_update_memory_used (src,
+ (int) (GST_BUFFER_SIZE(buffer) / 1024));
+
+ g_async_queue_push (src->queue, gst_buffer_ref (buffer));
+}
+
+/**
+ * shell_recorder_src_close:
+ *
+ * Indicates the end of the input stream. Once all previously added buffers have
+ * been pushed out an end-of-stream message will be sent.
+ */
+void
+shell_recorder_src_close (ShellRecorderSrc *src)
+{
+ /* We can't send a message to the source immediately or buffers that haven't
+ * been pushed yet will be discarded. Instead stick a marker onto our own
+ * queue to send an event once everything has been pushed.
+ */
+ g_async_queue_push (src->queue, RECORDER_QUEUE_END);
+}
+
+static gboolean
+plugin_init (GstPlugin *plugin)
+{
+ gst_element_register(plugin, "shellrecordersrc", GST_RANK_NONE,
+ SHELL_TYPE_RECORDER_SRC);
+
+ return TRUE;
+}
+
+/**
+ * shell_recorder_src_register:
+ * Registers a plugin holding our single element to use privately in
+ * this application. Can safely be called multiple times.
+ */
+void
+shell_recorder_src_register (void)
+{
+ static gboolean registered = FALSE;
+ if (registered)
+ return;
+
+ gst_plugin_register_static (GST_VERSION_MAJOR, GST_VERSION_MINOR,
+ "shellrecorder",
+ "Plugin for ShellRecorder",
+ plugin_init,
+ "0.1",
+ "LGPL",
+ "gnome-shell", "gnome-shell", "http://live.gnome.org/GnomeShell");
+
+ registered = TRUE;
+}
diff --git a/src/shell-recorder-src.h b/src/shell-recorder-src.h
new file mode 100644
index 0000000..d478220
--- /dev/null
+++ b/src/shell-recorder-src.h
@@ -0,0 +1,39 @@
+#ifndef __SHELL_RECORDER_SRC_H__
+#define __SHELL_RECORDER_SRC_H__
+
+#include <gst/gst.h>
+
+G_BEGIN_DECLS
+
+/**
+ * ShellRecorderSrc:
+ *
+ * shellrecordersrc a custom source element is pretty much like a very
+ * simple version of the stander GStreamer 'appsrc' element, without
+ * any of the provisions for seeking, generating data on demand,
+ * etc. In both cases, the application supplies the buffers and the
+ * element pushes them into the pipeline. The main reason for not using
+ * appsrc is that it wasn't a supported element until gstreamer 0.10.22,
+ * and as of 2009-03, many systems still have 0.10.21.
+ */
+typedef struct _ShellRecorderSrc ShellRecorderSrc;
+typedef struct _ShellRecorderSrcClass ShellRecorderSrcClass;
+
+#define SHELL_TYPE_RECORDER_SRC (shell_recorder_src_get_type ())
+#define SHELL_RECORDER_SRC(object) (G_TYPE_CHECK_INSTANCE_CAST ((object), SHELL_TYPE_RECORDER_SRC, ShellRecorderSrc))
+#define SHELL_RECORDER_SRC_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), SHELL_TYPE_RECORDER_SRC, ShellRecorderSrcClass))
+#define SHELL_IS_RECORDER_SRC(object) (G_TYPE_CHECK_INSTANCE_TYPE ((object), SHELL_TYPE_RECORDER_SRC))
+#define SHELL_IS_RECORDER_SRC_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), SHELL_TYPE_RECORDER_SRC))
+#define SHELL_RECORDER_SRC_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), SHELL_TYPE_RECORDER_SRC, ShellRecorderSrcClass))
+
+GType shell_recorder_src_get_type (void) G_GNUC_CONST;
+
+void shell_recorder_src_register (void);
+
+void shell_recorder_src_add_buffer (ShellRecorderSrc *src,
+ GstBuffer *buffer);
+void shell_recorder_src_close (ShellRecorderSrc *src);
+
+G_END_DECLS
+
+#endif /* __SHELL_RECORDER_SRC_H__ */
diff --git a/src/shell-recorder.c b/src/shell-recorder.c
new file mode 100644
index 0000000..b80b7db
--- /dev/null
+++ b/src/shell-recorder.c
@@ -0,0 +1,1666 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+
+#include <fcntl.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <gst/gst.h>
+
+#include "shell-recorder-src.h"
+#include "shell-recorder.h"
+
+#include <clutter/x11/clutter-x11.h>
+#include <X11/extensions/Xfixes.h>
+
+typedef enum {
+ RECORDER_STATE_CLOSED,
+ RECORDER_STATE_PAUSED,
+ 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;
+ char *unique; /* The unique string we are using for this recording */
+ int count; /* How many times the recording has been started */
+
+ ClutterStage *stage;
+ int stage_width;
+ int stage_height;
+
+ gboolean have_pointer;
+ int pointer_x;
+ int pointer_y;
+
+ gboolean have_xfixes;
+ int xfixes_event_base;
+
+ CoglHandle *recording_icon; /* icon shown while playing */
+
+ cairo_surface_t *cursor_image;
+ int cursor_hot_x;
+ int cursor_hot_y;
+
+ gboolean only_paint; /* Used to temporarily suppress recording */
+
+ char *pipeline_description;
+ char *filename;
+ gboolean filename_has_count; /* %c used: handle pausing differently */
+
+ /* 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 (adjusted for pauses) */
+ GstClockTime pause_time; /* When the pipeline was paused */
+
+ /* GSource IDs for different timeouts and idles */
+ guint redraw_timeout;
+ guint redraw_idle;
+ guint update_memory_used_timeout;
+ guint update_pointer_timeout;
+};
+
+struct _RecorderPipeline
+{
+ ShellRecorder *recorder;
+ GstElement *pipeline;
+ GstElement *src;
+ int outfile;
+};
+
+static void recorder_set_stage (ShellRecorder *recorder,
+ ClutterStage *stage);
+static void recorder_set_pipeline (ShellRecorder *recorder,
+ const char *pipeline);
+static void recorder_set_filename (ShellRecorder *recorder,
+ const char *filename);
+
+static void recorder_pipeline_set_caps (RecorderPipeline *pipeline);
+static void recorder_pipeline_closed (RecorderPipeline *pipeline);
+
+enum {
+ PROP_0,
+ PROP_STAGE,
+ PROP_PIPELINE,
+ PROP_FILENAME
+};
+
+G_DEFINE_TYPE(ShellRecorder, shell_recorder, G_TYPE_OBJECT);
+
+/* The number of frames per second we configure for the GStreamer pipeline.
+ * (the number of frames we actually write into the GStreamer pipeline is
+ * based entirely on how fast clutter is drawing.) Using 60fps seems high
+ * but the observed smoothness is a lot better than for 30fps when encoding
+ * as theora for a minimal size increase. This may be an artifact of the
+ * encoding process.
+ */
+#define FRAMES_PER_SECOND 15
+
+/* 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 "videorate ! theoraenc ! oggmux"
+
+/* The default filename pattern. Example shell-20090311b-2.ogg
+ */
+#define DEFAULT_FILENAME "shell-%d%u-%c.ogg"
+
+/* 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, 63,
+ COGL_TEXTURE_NONE,
+ COGL_PIXEL_FORMAT_BGRA_8888,
+ 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 */
+ fgets(line_buffer, sizeof(line_buffer), f);
+ }
+
+ fclose(f);
+
+ return DEFAULT_MEMORY_TARGET;
+}
+
+static void
+shell_recorder_init (ShellRecorder *recorder)
+{
+ shell_recorder_src_register ();
+
+ recorder->recording_icon = create_recording_icon ();
+ recorder->memory_target = get_memory_target();
+
+ recorder->state = RECORDER_STATE_CLOSED;
+}
+
+static void
+shell_recorder_finalize (GObject *object)
+{
+ ShellRecorder *recorder = SHELL_RECORDER (object);
+ GSList *l;
+
+ for (l = recorder->pipelines; l; l = l->next)
+ {
+ RecorderPipeline *pipeline = l->data;
+
+ /* Remove the back-reference. The pipeline will be freed
+ * when it finishes. (Or when the process exits, but that's
+ * out of our control.)
+ */
+ pipeline->recorder = NULL;
+ }
+
+ 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_filename (recorder, NULL);
+
+ cogl_texture_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_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 ());
+
+ 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];
+}
+
+/* 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)
+{
+ 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 < 0 ||
+ recorder->pointer_y < 0 ||
+ recorder->pointer_x >= recorder->stage_width ||
+ recorder->pointer_y >= recorder->stage_height)
+ return;
+
+ if (!recorder->cursor_image)
+ recorder_fetch_cursor_image (recorder);
+
+ if (!recorder->cursor_image)
+ return;
+
+ surface = cairo_image_surface_create_for_data (GST_BUFFER_DATA(buffer),
+ CAIRO_FORMAT_ARGB32,
+ recorder->stage_width,
+ recorder->stage_height,
+ recorder->stage_width * 4);
+
+ /* The data we get from glReadPixels is "upside down", so transform
+ * our cairo drawing to match */
+ cr = cairo_create (surface);
+ cairo_translate(cr, 0, recorder->stage_height);
+ cairo_scale(cr, 1, -1);
+
+ cairo_set_source_surface (cr,
+ recorder->cursor_image,
+ recorder->pointer_x - recorder->cursor_hot_x,
+ recorder->pointer_y - recorder->cursor_hot_y);
+ cairo_paint (cr);
+
+ cairo_destroy (cr);
+ cairo_surface_destroy (surface);
+}
+
+/* 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;
+
+ 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 */
+ cogl_rectangle (recorder->stage_width - 64, recorder->stage_height - 10,
+ recorder->stage_width - 2, recorder->stage_height - 9);
+ cogl_rectangle (recorder->stage_width - 64, recorder->stage_height - 9,
+ recorder->stage_width - (63 - fill_level), recorder->stage_height - 3);
+ cogl_rectangle (recorder->stage_width - 3, recorder->stage_height - 9,
+ recorder->stage_width - 2, recorder->stage_height - 3);
+ cogl_rectangle (recorder->stage_width - 64, recorder->stage_height - 3,
+ recorder->stage_width - 2, recorder->stage_height - 2);
+}
+
+/* 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;
+
+ size = recorder->stage_width * recorder->stage_height * 4;
+ data = g_malloc (size);
+
+ buffer = gst_buffer_new();
+ GST_BUFFER_SIZE(buffer) = size;
+ GST_BUFFER_MALLOCDATA(buffer) = GST_BUFFER_DATA(buffer) = data;
+
+ GST_BUFFER_TIMESTAMP(buffer) = get_wall_time() - recorder->start_time;
+
+ glReadBuffer (GL_BACK_LEFT);
+ glReadPixels (0, 0,
+ recorder->stage_width, recorder->stage_height,
+ GL_BGRA,
+ GL_UNSIGNED_INT_8_8_8_8_REV,
+ data);
+
+ 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)
+ {
+ if (!recorder->only_paint)
+ recorder_record_frame (recorder);
+
+ cogl_set_source_texture (recorder->recording_icon);
+ cogl_rectangle (recorder->stage_width - 32, recorder->stage_height - 42,
+ recorder->stage_width, recorder->stage_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);
+}
+
+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 ClutterTimeline, 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_TIMELINE + 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;
+
+ if (xev->xany.window != clutter_x11_get_stage_window (recorder->stage))
+ return CLUTTER_X11_FILTER_CONTINUE;
+
+ 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 (xev->xany.type == MotionNotify)
+ {
+ recorder->pointer_x = xev->xmotion.x;
+ recorder->pointer_y = xev->xmotion.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 (xev->xany.type == EnterNotify &&
+ (xev->xcrossing.detail != NotifyVirtual &&
+ xev->xcrossing.detail != NotifyNonlinearVirtual))
+ {
+ recorder->have_pointer = TRUE;
+ recorder->pointer_x = xev->xcrossing.x;
+ recorder->pointer_y = xev->xcrossing.y;
+
+ recorder_queue_redraw (recorder);
+ }
+ else if (xev->xany.type == LeaveNotify &&
+ (xev->xcrossing.detail != NotifyVirtual &&
+ xev->xcrossing.detail != NotifyNonlinearVirtual))
+ {
+ recorder->have_pointer = FALSE;
+ recorder->pointer_x = xev->xcrossing.x;
+ recorder->pointer_y = xev->xcrossing.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);
+
+ /* While we are at it, add mouse events to the event mask; they will
+ * be there for the stage windows that Clutter creates by default, but
+ * maybe this stage was created differently. Since we've already
+ * retrieved the event mask, it's almost free.
+ */
+ XSelectInput(xdisplay, xwindow,
+ xwa.your_event_mask | EnterWindowMask | LeaveWindowMask | PointerMotionMask);
+}
+
+/* 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_set_stage (ShellRecorder *recorder,
+ ClutterStage *stage)
+{
+ if (recorder->stage == stage)
+ return;
+
+ if (recorder->current_pipeline)
+ shell_recorder_close (recorder);
+
+ if (recorder->stage)
+ {
+ 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;
+ }
+ }
+
+ recorder->stage = stage;
+
+ if (recorder->stage)
+ {
+ int error_base;
+
+ recorder->stage = stage;
+ 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);
+
+ 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);
+
+ recorder_get_initial_cursor_position (recorder);
+ }
+}
+
+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_filename (ShellRecorder *recorder,
+ const char *filename)
+{
+ if (filename == recorder->filename ||
+ (filename && recorder->filename && strcmp (recorder->filename, filename) == 0))
+ return;
+
+ if (recorder->current_pipeline)
+ shell_recorder_close (recorder);
+
+ if (recorder->filename)
+ g_free (recorder->filename);
+
+ recorder->filename = g_strdup (filename);
+
+ g_object_notify (G_OBJECT (recorder), "filename");
+}
+
+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_PIPELINE:
+ recorder_set_pipeline (recorder, g_value_get_string (value));
+ break;
+ case PROP_FILENAME:
+ recorder_set_filename (recorder, g_value_get_string (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_PIPELINE:
+ g_value_set_string (value, recorder->pipeline_description);
+ break;
+ case PROP_FILENAME:
+ g_value_set_string (value, recorder->filename);
+ 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_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_FILENAME,
+ g_param_spec_string ("filename",
+ "Filename",
+ "The filename template to use for output files",
+ NULL,
+ 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; ffmpegcolorspace
+ * doesn't support little-endian xRGB, but does support
+ * big-endian BGRx.
+ */
+ caps = gst_caps_new_simple ("video/x-raw-rgb",
+ "bpp", G_TYPE_INT, 32,
+ "depth", G_TYPE_INT, 24,
+#if G_BYTE_ORDER == G_LITTLE_ENDIAN
+ "red_mask", G_TYPE_INT, 0x0000ff00,
+ "green_mask", G_TYPE_INT, 0x00ff0000,
+ "blue_mask", G_TYPE_INT, 0xff000000,
+#else
+ "red_mask", G_TYPE_INT, 0xff0000,
+ "green_mask", G_TYPE_INT, 0x00ff00,
+ "blue_mask", G_TYPE_INT, 0x0000ff,
+#endif
+ "endianness", G_TYPE_INT, G_BIG_ENDIAN,
+ "framerate", GST_TYPE_FRACTION, FRAMES_PER_SECOND, 1,
+ "width", G_TYPE_INT, pipeline->recorder->stage_width,
+ "height", G_TYPE_INT, pipeline->recorder->stage_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 *ffmpegcolorspace;
+ GstElement *videoflip;
+ GError *error = NULL;
+
+ 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 ffmpegcolorspace element is a generic converter; it will convert
+ * our supplied fixed format data into whatever the encoder wants
+ */
+ ffmpegcolorspace = gst_element_factory_make ("ffmpegcolorspace", NULL);
+ if (!ffmpegcolorspace)
+ {
+ g_warning("Can't create ffmpegcolorspace element");
+ goto out;
+ }
+ gst_bin_add (GST_BIN (pipeline->pipeline), ffmpegcolorspace);
+
+ /* glReadPixels gives us an upside-down buffer, so we have to flip it back
+ * right-side up. We do this after the color space conversion in the theory
+ * that we might have a smaller buffer to flip; on the other hand flipping
+ * YUV 422 is more complicated than flipping RGB. Probably a toss-up.
+ *
+ * We use gst_parse_launch to avoid having to know the enum value for flip-vertical
+ */
+ videoflip = gst_parse_launch_full ("videoflip method=vertical-flip", NULL,
+ GST_PARSE_FLAG_FATAL_ERRORS,
+ &error);
+ if (videoflip == NULL)
+ {
+ g_warning("Can't create videoflip element: %s", error->message);
+ g_error_free (error);
+ goto out;
+ }
+ gst_bin_add (GST_BIN (pipeline->pipeline), videoflip);
+
+ gst_element_link_many (pipeline->src, ffmpegcolorspace, videoflip,
+ NULL);
+
+ src_pad = gst_element_get_static_pad (videoflip, "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;
+}
+
+/* Counts '', 'a', ..., 'z', 'aa', ..., 'az', 'ba', ... */
+static void
+increment_unique (GString *unique)
+{
+ int i;
+
+ for (i = unique->len - 1; i >= 0; i--)
+ {
+ if (unique->str[i] != 'z')
+ {
+ unique->str[i]++;
+ return;
+ }
+ else
+ unique->str[i] = 'a';
+ }
+
+ g_string_prepend_c (unique, 'a');
+}
+
+static char *
+get_absolute_path (char *maybe_relative)
+{
+ char *path;
+
+ if (g_path_is_absolute (maybe_relative))
+ path = g_strdup (maybe_relative);
+ else
+ {
+ char *cwd = g_get_current_dir ();
+ path = g_build_filename (cwd, maybe_relative, NULL);
+ g_free (cwd);
+ }
+
+ 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)
+{
+ GString *unique = g_string_new (NULL); /* add to filename to make it unique */
+ const char *pattern;
+ int flags;
+ int outfile = -1;
+
+ recorder->count++;
+
+ pattern = recorder->filename;
+ if (!pattern)
+ pattern = DEFAULT_FILENAME;
+
+ while (TRUE)
+ {
+ GString *filename = g_string_new (NULL);
+ const char *p;
+
+ for (p = pattern; *p; p++)
+ {
+ if (*p == '%')
+ {
+ switch (*(p + 1))
+ {
+ case '%':
+ case '\0':
+ g_string_append_c (filename, '%');
+ break;
+ case 'c':
+ {
+ /* Count distinguishing multiple files created in session */
+ g_string_append_printf (filename, "%d", recorder->count);
+ recorder->filename_has_count = TRUE;
+ }
+ break;
+ case 'd':
+ {
+ /* Appends date as YYYYMMDD */
+ GDate date;
+ GTimeVal now;
+ g_get_current_time (&now);
+ g_date_clear (&date, 1);
+ g_date_set_time_val (&date, &now);
+ g_string_append_printf (filename, "%04d%02d%02d",
+ g_date_get_year (&date),
+ g_date_get_month (&date),
+ g_date_get_day (&date));
+ }
+ break;
+ case 'u':
+ if (recorder->unique)
+ g_string_append (filename, recorder->unique);
+ else
+ g_string_append (filename, unique->str);
+ break;
+ default:
+ g_warning ("Unknown escape %%%c in filename", *p);
+ 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;
+ if (recorder->filename_has_count)
+ flags |= O_EXCL;
+
+ outfile = open (filename->str, flags, 0666);
+ if (outfile != -1)
+ {
+ char *path = get_absolute_path (filename->str);
+ g_printerr ("Recording to %s\n", path);
+ g_free (path);
+
+ g_string_free (filename, TRUE);
+ goto out;
+ }
+
+ if (outfile == -1 &&
+ (errno != EEXIST || !recorder->filename_has_count))
+ {
+ g_warning ("Cannot open output file '%s': %s", filename->str, g_strerror (errno));
+ g_string_free (filename, TRUE);
+ goto out;
+ }
+
+ if (recorder->unique)
+ {
+ /* We've already picked a unique string based on count=1, and now we had a collision
+ * for a subsequent count.
+ */
+ g_warning ("Name collision with existing file for '%s'", filename->str);
+ g_string_free (filename, TRUE);
+ goto out;
+ }
+
+ g_string_free (filename, TRUE);
+
+ increment_unique (unique);
+ }
+
+ out:
+ if (outfile != -1)
+ recorder->unique = g_string_free (unique, FALSE);
+ else
+ g_string_free (unique, TRUE);
+
+ 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);
+ 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);
+}
+
+/* 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);
+
+ gst_element_set_state (pipeline->pipeline, GST_STATE_NULL);
+
+ if (pipeline->recorder)
+ {
+ ShellRecorder *recorder = pipeline->recorder;
+ if (pipeline == recorder->current_pipeline)
+ {
+ /* Error case; force a close */
+ recorder->current_pipeline = NULL;
+ shell_recorder_close (recorder);
+ }
+
+ recorder->pipelines = g_slist_remove (recorder->pipelines, pipeline);
+ }
+
+ recorder_pipeline_free (pipeline);
+}
+
+static gboolean
+recorder_open_pipeline (ShellRecorder *recorder)
+{
+ RecorderPipeline *pipeline;
+ const char *pipeline_description;
+ GError *error = NULL;
+ GstBus *bus;
+
+ pipeline = g_new0(RecorderPipeline, 1);
+ pipeline->recorder = recorder;
+ pipeline->outfile = - 1;
+
+ pipeline_description = recorder->pipeline_description;
+ if (!pipeline_description)
+ pipeline_description = DEFAULT_PIPELINE;
+
+ pipeline->pipeline = gst_parse_launch_full (pipeline_description, NULL,
+ GST_PARSE_FLAG_FATAL_ERRORS,
+ &error);
+
+ 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;
+ recorder->filename_has_count = FALSE;
+ }
+}
+
+/**
+ * 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_filename:
+ * @recorder: the #ShellRecorder
+ * @filename: 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
+ * %u: A string added to make the filename unique.
+ * '', 'a', 'b', ... 'aa', 'ab', ..
+ * %c: A counter that is updated (opening a new file) each
+ * time the recording stream is paused.
+ * %%: A literal percent
+ *
+ * The default value is 'shell-%d%u-%c.ogg'.
+ */
+void
+shell_recorder_set_filename (ShellRecorder *recorder,
+ const char *filename)
+{
+ g_return_if_fail (SHELL_IS_RECORDER (recorder));
+
+ recorder_set_filename (recorder, filename);
+
+}
+
+/**
+ * shell_recorder_set_pipeline:
+ * @recorder: the #ShellRecorder
+ * @filename: the GStreamer pipeline used to encode recordings
+ * or %NULL for the defalt 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_filename().) 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 'videorate ! theoraenc ! oggmux'
+ */
+void
+shell_recorder_set_pipeline (ShellRecorder *recorder,
+ const char *pipeline)
+{
+ g_return_if_fail (SHELL_IS_RECORDER (recorder));
+
+ recorder_set_pipeline (recorder, pipeline);
+}
+
+/**
+ * shell_recorder_record:
+ * @recorder: the #ShellRecorder
+ *
+ * Starts recording, or continues a recording that was previously
+ * paused. 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)
+{
+ 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->current_pipeline)
+ {
+ /* Adjust the start time so that the times in the stream ignore the
+ * pause
+ */
+ recorder->start_time = recorder->start_time + (get_wall_time() - recorder->pause_time);
+ }
+ else
+ {
+ if (!recorder_open_pipeline (recorder))
+ return FALSE;
+
+ recorder->start_time = get_wall_time();
+ }
+
+ recorder->state = RECORDER_STATE_RECORDING;
+ recorder_add_update_pointer_timeout (recorder);
+
+ /* 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_pause:
+ * @recorder: the #ShellRecorder
+ *
+ * Temporarily stop recording. If the specified filename includes
+ * the %c escape, then the stream is closed and a new stream with
+ * an incremented counter will be created. Otherwise the stream
+ * is paused and will be continued when shell_recorder_record()
+ * is next called.
+ */
+void
+shell_recorder_pause (ShellRecorder *recorder)
+{
+ g_return_if_fail (SHELL_IS_RECORDER (recorder));
+ g_return_if_fail (recorder->state == RECORDER_STATE_RECORDING);
+
+ recorder_remove_update_pointer_timeout (recorder);
+ /* We want to record one more frame since some time may have
+ * elapsed since the last frame
+ */
+ clutter_actor_paint (CLUTTER_ACTOR (recorder->stage));
+
+ if (recorder->filename_has_count)
+ recorder_close_pipeline (recorder);
+
+ recorder->state = RECORDER_STATE_PAUSED;
+ recorder->pause_time = get_wall_time();
+
+ /* Queue a redraw to remove the recording indicator */
+ clutter_actor_queue_redraw (CLUTTER_ACTOR (recorder->stage));
+}
+
+/**
+ * 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);
+
+ if (recorder->state == RECORDER_STATE_RECORDING)
+ shell_recorder_pause (recorder);
+
+ recorder_remove_update_pointer_timeout (recorder);
+ recorder_remove_redraw_timeout (recorder);
+ recorder_close_pipeline (recorder);
+
+ recorder->state = RECORDER_STATE_CLOSED;
+ recorder->count = 0;
+ g_free (recorder->unique);
+ recorder->unique = NULL;
+
+ /* 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;
+}
diff --git a/src/shell-recorder.h b/src/shell-recorder.h
new file mode 100644
index 0000000..359e528
--- /dev/null
+++ b/src/shell-recorder.h
@@ -0,0 +1,43 @@
+#ifndef __SHELL_RECORDER_H__
+#define __SHELL_RECORDER_H__
+
+#include <clutter/clutter.h>
+
+G_BEGIN_DECLS
+
+/**
+ * SECTION:ShellRecorder
+ * short_description: Record from a #ClutterStage
+ *
+ * The #ShellRecorder object is used to make recordings ("screencasts")
+ * of a #ClutterStage. Recording is done via #GStreamer. The default is
+ * to encode as a Theora movie and write it to a file in the current
+ * directory named after the date, but the encoding and output can
+ * be configured.
+ */
+typedef struct _ShellRecorder ShellRecorder;
+typedef struct _ShellRecorderClass ShellRecorderClass;
+
+#define SHELL_TYPE_RECORDER (shell_recorder_get_type ())
+#define SHELL_RECORDER(object) (G_TYPE_CHECK_INSTANCE_CAST ((object), SHELL_TYPE_RECORDER, ShellRecorder))
+#define SHELL_RECORDER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), SHELL_TYPE_RECORDER, ShellRecorderClass))
+#define SHELL_IS_RECORDER(object) (G_TYPE_CHECK_INSTANCE_TYPE ((object), SHELL_TYPE_RECORDER))
+#define SHELL_IS_RECORDER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), SHELL_TYPE_RECORDER))
+#define SHELL_RECORDER_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), SHELL_TYPE_RECORDER, ShellRecorderClass))
+
+GType shell_recorder_get_type (void) G_GNUC_CONST;
+
+ShellRecorder *shell_recorder_new (ClutterStage *stage);
+
+void shell_recorder_set_filename (ShellRecorder *recorder,
+ const char *filename);
+void shell_recorder_set_pipeline (ShellRecorder *recorder,
+ const char *pipeline);
+gboolean shell_recorder_record (ShellRecorder *recorder);
+void shell_recorder_close (ShellRecorder *recorder);
+void shell_recorder_pause (ShellRecorder *recorder);
+gboolean shell_recorder_is_recording (ShellRecorder *recorder);
+
+G_END_DECLS
+
+#endif /* __SHELL_RECORDER_H__ */
diff --git a/src/test-recorder.c b/src/test-recorder.c
new file mode 100644
index 0000000..d8da9c5
--- /dev/null
+++ b/src/test-recorder.c
@@ -0,0 +1,95 @@
+#include "shell-recorder.h"
+#include <clutter/clutter.h>
+#include <gst/gst.h>
+
+/* Very simple test of the ShellRecorder class; shows some text strings
+ * moving around and records it.
+ */
+static ShellRecorder *recorder;
+
+static gboolean
+stop_recording_timeout (gpointer data)
+{
+ shell_recorder_close (recorder);
+ return FALSE;
+}
+
+static void
+on_animation_completed (ClutterAnimation *animation)
+{
+ g_timeout_add (1000, stop_recording_timeout, NULL);
+}
+
+int main (int argc, char **argv)
+{
+ ClutterActor *stage;
+ ClutterActor *text;
+ ClutterAnimation *animation;
+ ClutterColor red, green, blue;
+
+ g_thread_init (NULL);
+ gst_init (&argc, &argv);
+ clutter_init (&argc, &argv);
+
+ clutter_color_from_string (&red, "red");
+ clutter_color_from_string (&green, "green");
+ clutter_color_from_string (&blue, "blue");
+ stage = clutter_stage_get_default ();
+
+ text = g_object_new (CLUTTER_TYPE_TEXT,
+ "text", "Red",
+ "font-name", "Sans 40px",
+ "color", &red,
+ NULL);
+ clutter_container_add_actor (CLUTTER_CONTAINER (stage), text);
+ animation = clutter_actor_animate (text,
+ CLUTTER_EASE_IN_OUT_QUAD,
+ 3000,
+ "x", 320,
+ "y", 240,
+ NULL);
+ g_signal_connect (animation, "completed",
+ G_CALLBACK (on_animation_completed), NULL);
+
+ text = g_object_new (CLUTTER_TYPE_TEXT,
+ "text", "Blue",
+ "font-name", "Sans 40px",
+ "color", &blue,
+ "x", 640,
+ "y", 0,
+ NULL);
+ clutter_actor_set_anchor_point_from_gravity (text, CLUTTER_GRAVITY_NORTH_EAST);
+ clutter_container_add_actor (CLUTTER_CONTAINER (stage), text);
+ animation = clutter_actor_animate (text,
+ CLUTTER_EASE_IN_OUT_QUAD,
+ 3000,
+ "x", 320,
+ "y", 240,
+ NULL);
+
+ text = g_object_new (CLUTTER_TYPE_TEXT,
+ "text", "Green",
+ "font-name", "Sans 40px",
+ "color", &green,
+ "x", 0,
+ "y", 480,
+ NULL);
+ clutter_actor_set_anchor_point_from_gravity (text, CLUTTER_GRAVITY_SOUTH_WEST);
+ clutter_container_add_actor (CLUTTER_CONTAINER (stage), text);
+ animation = clutter_actor_animate (text,
+ CLUTTER_EASE_IN_OUT_QUAD,
+ 3000,
+ "x", 320,
+ "y", 240,
+ NULL);
+
+ recorder = shell_recorder_new (CLUTTER_STAGE (stage));
+ shell_recorder_set_filename (recorder, "test-recorder.ogg");
+
+ clutter_actor_show (stage);
+
+ shell_recorder_record (recorder);
+ clutter_main ();
+
+ return 0;
+}
diff --git a/tools/build/gnome-shell-build-setup.sh b/tools/build/gnome-shell-build-setup.sh
index ccabdf6..cf00c8f 100755
--- a/tools/build/gnome-shell-build-setup.sh
+++ b/tools/build/gnome-shell-build-setup.sh
@@ -71,6 +71,7 @@ if test x$system = xFedora ; then
librsvg2-devel libwnck-devel mesa-libGL-devel python-devel readline-devel \
xulrunner-devel libXdamage-devel \
gdb glx-utils xorg-x11-apps xorg-x11-server-Xephyr xterm zenity \
+ gstreamer-devel gstreamer-plugins-base gstreamer-plugins-good \
; do
if ! rpm -q $pkg > /dev/null 2>&1; then
reqd="$pkg $reqd"
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]