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
This commit is contained in:
Owen W. Taylor 2009-03-13 17:14:31 -04:00
parent 288fb7a837
commit afceea3fe6
10 changed files with 2210 additions and 1 deletions

2
.gitignore vendored
View File

@ -26,4 +26,6 @@ src/Makefile
src/Makefile.in src/Makefile.in
src/gnomeshell-taskpanel src/gnomeshell-taskpanel
src/gnome-shell src/gnome-shell
src/test-recorder
src/test-recorder.ogg
stamp-h1 stamp-h1

View File

@ -18,7 +18,27 @@ AC_SUBST(GETTEXT_PACKAGE)
AC_DEFINE_UNQUOTED(GETTEXT_PACKAGE, "$GETTEXT_PACKAGE", AC_DEFINE_UNQUOTED(GETTEXT_PACKAGE, "$GETTEXT_PACKAGE",
[The prefix for our gettext translation domains.]) [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(TIDY, clutter-0.9)
PKG_CHECK_MODULES(BIG, clutter-0.9 gtk+-2.0 librsvg-2.0) PKG_CHECK_MODULES(BIG, clutter-0.9 gtk+-2.0 librsvg-2.0)
PKG_CHECK_MODULES(GDMUSER, dbus-glib-1 gtk+-2.0) PKG_CHECK_MODULES(GDMUSER, dbus-glib-1 gtk+-2.0)

View File

@ -20,6 +20,7 @@ let overlay = null;
let overlayActive = false; let overlayActive = false;
let runDialog = null; let runDialog = null;
let wm = null; let wm = null;
let recorder = null;
function start() { function start() {
let global = Shell.Global.get(); let global = Shell.Global.get();
@ -71,6 +72,24 @@ function start() {
show_overlay(); 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); display.connect('overlay-key', toggleOverlay);
global.connect('panel-main-menu', toggleOverlay); global.connect('panel-main-menu', toggleOverlay);

View File

@ -66,6 +66,32 @@ libgnome_shell_la_SOURCES = \
# ClutterGLXTexturePixmap is currently not wrapped # ClutterGLXTexturePixmap is currently not wrapped
non_gir_sources = shell-gtkwindow-actor.h 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 = \ libgnome_shell_la_gir_sources = \
$(filter-out $(non_gir_sources), $(libgnome_shell_la_SOURCES)) $(filter-out $(non_gir_sources), $(libgnome_shell_la_SOURCES))

298
src/shell-recorder-src.c Normal file
View File

@ -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;
}

39
src/shell-recorder-src.h Normal file
View File

@ -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__ */

1666
src/shell-recorder.c Normal file

File diff suppressed because it is too large Load Diff

43
src/shell-recorder.h Normal file
View File

@ -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__ */

95
src/test-recorder.c Normal file
View File

@ -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;
}

View File

@ -71,6 +71,7 @@ if test x$system = xFedora ; then
librsvg2-devel libwnck-devel mesa-libGL-devel python-devel readline-devel \ librsvg2-devel libwnck-devel mesa-libGL-devel python-devel readline-devel \
xulrunner-devel libXdamage-devel \ xulrunner-devel libXdamage-devel \
gdb glx-utils xorg-x11-apps xorg-x11-server-Xephyr xterm zenity \ gdb glx-utils xorg-x11-apps xorg-x11-server-Xephyr xterm zenity \
gstreamer-devel gstreamer-plugins-base gstreamer-plugins-good \
; do ; do
if ! rpm -q $pkg > /dev/null 2>&1; then if ! rpm -q $pkg > /dev/null 2>&1; then
reqd="$pkg $reqd" reqd="$pkg $reqd"