[gnome-shell] Add a built-in screencast recording facility



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]