diff --git a/data/dbus-interfaces/org.gnome.Mutter.ScreenCast.xml b/data/dbus-interfaces/org.gnome.Mutter.ScreenCast.xml
new file mode 100644
index 000000000..a8ff3ccb9
--- /dev/null
+++ b/data/dbus-interfaces/org.gnome.Mutter.ScreenCast.xml
@@ -0,0 +1,191 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/data/gnome-shell-dbus-interfaces.gresource.xml b/data/gnome-shell-dbus-interfaces.gresource.xml
index 8d335124a..69bc67458 100644
--- a/data/gnome-shell-dbus-interfaces.gresource.xml
+++ b/data/gnome-shell-dbus-interfaces.gresource.xml
@@ -28,6 +28,7 @@
org.freedesktop.UPower.xml
org.gnome.Magnifier.xml
org.gnome.Magnifier.ZoomRegion.xml
+ org.gnome.Mutter.ScreenCast.xml
org.gnome.ScreenSaver.xml
org.gnome.SessionManager.EndSessionDialog.xml
org.gnome.SessionManager.Inhibitor.xml
diff --git a/docs/reference/shell/meson.build b/docs/reference/shell/meson.build
index 9d8bbfd19..5936ea3f6 100644
--- a/docs/reference/shell/meson.build
+++ b/docs/reference/shell/meson.build
@@ -3,13 +3,8 @@ private_headers = [
'gactionobservable.h',
'gactionobserver.h',
'shell-network-agent.h',
- 'shell-recorder-src.h'
]
-if not enable_recorder
- private_headers += 'shell-recorder.h'
-endif
-
exclude_directories = [
'calendar-server',
'hotplug-sniffer',
diff --git a/js/dbusServices/meson.build b/js/dbusServices/meson.build
index c749f45dc..2f047bb52 100644
--- a/js/dbusServices/meson.build
+++ b/js/dbusServices/meson.build
@@ -8,6 +8,12 @@ dbus_services = {
'org.gnome.Shell.Notifications': 'notifications',
}
+if enable_recorder
+ dbus_services += {
+ 'org.gnome.Shell.Screencast': 'screencast',
+ }
+endif
+
config_dir = '@0@/..'.format(meson.current_build_dir())
foreach service, dir : dbus_services
diff --git a/js/dbusServices/org.gnome.Shell.Screencast.src.gresource.xml b/js/dbusServices/org.gnome.Shell.Screencast.src.gresource.xml
new file mode 100644
index 000000000..e99b1ac18
--- /dev/null
+++ b/js/dbusServices/org.gnome.Shell.Screencast.src.gresource.xml
@@ -0,0 +1,11 @@
+
+
+
+ main.js
+ screencastService.js
+ dbusService.js
+
+ misc/config.js
+ misc/fileUtils.js
+
+
diff --git a/js/dbusServices/screencast/main.js b/js/dbusServices/screencast/main.js
new file mode 100644
index 000000000..4a244264f
--- /dev/null
+++ b/js/dbusServices/screencast/main.js
@@ -0,0 +1,11 @@
+/* exported main */
+
+const { DBusService } = imports.dbusService;
+const { ScreencastService } = imports.screencastService;
+
+function main() {
+ const service = new DBusService(
+ 'org.gnome.Shell.Screencast',
+ new ScreencastService());
+ service.run();
+}
diff --git a/js/dbusServices/screencast/screencastService.js b/js/dbusServices/screencast/screencastService.js
new file mode 100644
index 000000000..99ad59f2e
--- /dev/null
+++ b/js/dbusServices/screencast/screencastService.js
@@ -0,0 +1,458 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported ScreencastService */
+
+const { Gio, GLib, Gst } = imports.gi;
+
+const { loadInterfaceXML, loadSubInterfaceXML } = imports.misc.fileUtils;
+const { ServiceImplementation } = imports.dbusService;
+
+const ScreencastIface = loadInterfaceXML('org.gnome.Shell.Screencast');
+
+const IntrospectIface = loadInterfaceXML('org.gnome.Shell.Introspect');
+const IntrospectProxy = Gio.DBusProxy.makeProxyWrapper(IntrospectIface);
+
+const ScreenCastIface = loadSubInterfaceXML(
+ 'org.gnome.Mutter.ScreenCast', 'org.gnome.Mutter.ScreenCast');
+const ScreenCastSessionIface = loadSubInterfaceXML(
+ 'org.gnome.Mutter.ScreenCast.Session', 'org.gnome.Mutter.ScreenCast');
+const ScreenCastStreamIface = loadSubInterfaceXML(
+ 'org.gnome.Mutter.ScreenCast.Stream', 'org.gnome.Mutter.ScreenCast');
+const ScreenCastProxy = Gio.DBusProxy.makeProxyWrapper(ScreenCastIface);
+const ScreenCastSessionProxy = Gio.DBusProxy.makeProxyWrapper(ScreenCastSessionIface);
+const ScreenCastStreamProxy = Gio.DBusProxy.makeProxyWrapper(ScreenCastStreamIface);
+
+const DEFAULT_PIPELINE = 'vp8enc min_quantizer=13 max_quantizer=13 cpu-used=5 deadline=1000000 threads=%T ! queue ! webmmux';
+const DEFAULT_FRAMERATE = 30;
+const DEFAULT_DRAW_CURSOR = true;
+
+const PipelineState = {
+ INIT: 0,
+ PLAYING: 1,
+ FLUSHING: 2,
+ STOPPED: 3,
+};
+
+const SessionState = {
+ INIT: 0,
+ ACTIVE: 1,
+ STOPPED: 2,
+};
+
+var Recorder = class {
+ constructor(sessionPath, x, y, width, height, filePath, options,
+ invocation,
+ onErrorCallback) {
+ this._startInvocation = invocation;
+ this._dbusConnection = invocation.get_connection();
+ this._onErrorCallback = onErrorCallback;
+ this._stopInvocation = null;
+
+ this._pipelineIsPlaying = false;
+ this._sessionIsActive = false;
+
+ this._x = x;
+ this._y = y;
+ this._width = width;
+ this._height = height;
+ this._filePath = filePath;
+
+ this._pipelineString = DEFAULT_PIPELINE;
+ this._framerate = DEFAULT_FRAMERATE;
+ this._drawCursor = DEFAULT_DRAW_CURSOR;
+
+ this._applyOptions(options);
+ this._watchSender(invocation.get_sender());
+
+ this._initSession(sessionPath);
+ }
+
+ _applyOptions(options) {
+ for (const option in options)
+ options[option] = options[option].deep_unpack();
+
+ if (options['pipeline'] !== undefined)
+ this._pipelineString = options['pipeline'];
+ if (options['framerate'] !== undefined)
+ this._framerate = options['framerate'];
+ if ('draw-cursor' in options)
+ this._drawCursor = options['draw-cursor'];
+ }
+
+ _watchSender(sender) {
+ this._nameWatchId = this._dbusConnection.watch_name(
+ sender,
+ Gio.BusNameWatcherFlags.NONE,
+ null,
+ this._senderVanished.bind(this));
+ }
+
+ _unwatchSender() {
+ if (this._nameWatchId !== 0) {
+ this._dbusConnection.unwatch_name(this._nameWatchId);
+ this._nameWatchId = 0;
+ }
+ }
+
+ _senderVanished() {
+ this._unwatchSender();
+
+ this.stopRecording(null);
+ }
+
+ _notifyStopped() {
+ this._unwatchSender();
+ if (this._onStartedCallback)
+ this._onStartedCallback(this, false);
+ else if (this._onStoppedCallback)
+ this._onStoppedCallback(this);
+ else
+ this._onErrorCallback(this);
+ }
+
+ _onSessionClosed() {
+ switch (this._pipelineState) {
+ case PipelineState.STOPPED:
+ break;
+ default:
+ this._pipeline.set_state(Gst.State.NULL);
+ log(`Unexpected pipeline state: ${this._pipelineState}`);
+ break;
+ }
+ this._notifyStopped();
+ }
+
+ _initSession(sessionPath) {
+ this._sessionProxy = new ScreenCastSessionProxy(Gio.DBus.session,
+ 'org.gnome.Mutter.ScreenCast',
+ sessionPath);
+ this._sessionProxy.connectSignal('Closed', this._onSessionClosed.bind(this));
+ }
+
+ _startPipeline(nodeId) {
+ this._ensurePipeline(nodeId);
+
+ const bus = this._pipeline.get_bus();
+ bus.add_watch(bus, this._onBusMessage.bind(this));
+
+ this._pipeline.set_state(Gst.State.PLAYING);
+ this._pipelineState = PipelineState.PLAYING;
+
+ this._onStartedCallback(this, true);
+ this._onStartedCallback = null;
+ }
+
+ startRecording(onStartedCallback) {
+ this._onStartedCallback = onStartedCallback;
+
+ const [streamPath] = this._sessionProxy.RecordAreaSync(
+ this._x, this._y,
+ this._width, this._height,
+ {
+ 'is-recording': GLib.Variant.new('b', true),
+ 'cursor-mode': GLib.Variant.new('u', this._drawCursor ? 1 : 0),
+ });
+
+ this._streamProxy = new ScreenCastStreamProxy(Gio.DBus.session,
+ 'org.gnome.ScreenCast.Stream',
+ streamPath);
+
+ this._streamProxy.connectSignal('PipeWireStreamAdded',
+ (proxy, sender, params) => {
+ const [nodeId] = params;
+ this._startPipeline(nodeId);
+ });
+ this._sessionProxy.StartSync();
+ this._sessionState = SessionState.ACTIVE;
+ }
+
+ stopRecording(onStoppedCallback) {
+ this._pipelineState = PipelineState.FLUSHING;
+ this._onStoppedCallback = onStoppedCallback;
+ this._pipeline.send_event(Gst.Event.new_eos());
+ }
+
+ _stopSession() {
+ this._sessionProxy.StopSync();
+ this._sessionState = SessionState.STOPPED;
+ }
+
+ _onBusMessage(bus, message, _) {
+ switch (message.type) {
+ case Gst.MessageType.EOS:
+ this._pipeline.set_state(Gst.State.NULL);
+
+ switch (this._pipelineState) {
+ case PipelineState.FLUSHING:
+ this._pipelineState = PipelineState.STOPPED;
+ break;
+ default:
+ break;
+ }
+
+ switch (this._sessionState) {
+ case SessionState.ACTIVE:
+ this._stopSession();
+ break;
+ case SessionState.STOPPED:
+ this._notifyStopped();
+ break;
+ default:
+ break;
+ }
+
+ break;
+ default:
+ break;
+ }
+ return true;
+ }
+
+ _substituteThreadCount(pipelineDescr) {
+ const numProcessors = GLib.get_num_processors();
+ const numThreads = Math.min(Math.max(1, numProcessors), 64);
+ return pipelineDescr.replace(/%T/, numThreads);
+ }
+
+ _ensurePipeline(nodeId) {
+ const framerate = this._framerate;
+
+ let fullPipeline = `
+ pipewiresrc path=${nodeId}
+ do-timestamp=true
+ keepalive-time=1000
+ resend-last=true !
+ video/x-raw,max-framerate=${framerate}/1 !
+ videoconvert !
+ ${this._pipelineString} !
+ filesink location=${this._filePath}`;
+ fullPipeline = this._substituteThreadCount(fullPipeline);
+
+ this._pipeline = Gst.parse_launch_full(fullPipeline,
+ null,
+ Gst.ParseFlags.FATAL_ERRORS);
+ }
+};
+
+var ScreencastService = class extends ServiceImplementation {
+ constructor() {
+ super(ScreencastIface, '/org/gnome/Shell/Screencast');
+
+ Gst.init(null);
+
+ this._recorders = new Map();
+ this._senders = new Map();
+
+ this._lockdownSettings = new Gio.Settings({
+ schema_id: 'org.gnome.desktop.lockdown',
+ });
+
+ this._proxy = new ScreenCastProxy(Gio.DBus.session,
+ 'org.gnome.Mutter.ScreenCast',
+ '/org/gnome/Mutter/ScreenCast');
+
+ this._introspectProxy = new IntrospectProxy(Gio.DBus.session,
+ 'org.gnome.Shell.Introspect',
+ '/org/gnome/Shell/Introspect');
+ }
+
+ _removeRecorder(sender) {
+ this._recorders.delete(sender);
+ if (this._recorders.size === 0)
+ this.release();
+ }
+
+ _addRecorder(sender, recorder) {
+ this._recorders.set(sender, recorder);
+ if (this._recorders.size === 1)
+ this.hold();
+ }
+
+ _getAbsolutePath(filename) {
+ if (GLib.path_is_absolute(filename))
+ return filename;
+
+ let videoDir = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_VIDEOS);
+ if (!GLib.file_test(videoDir, GLib.FileTest.EXISTS))
+ videoDir = GLib.get_home_dir();
+
+ return GLib.build_filenamev([videoDir, filename]);
+ }
+
+ _generateFilePath(template) {
+ let filename = '';
+ let escape = false;
+
+ [...template].forEach(c => {
+ if (escape) {
+ switch (c) {
+ case '%':
+ filename += '%';
+ break;
+ case 'd': {
+ const datetime = GLib.DateTime.new_now_local();
+ const datestr = datetime.format('%0x');
+ const datestrEscaped = datestr.replace(/\//g, '-');
+
+ filename += datestrEscaped;
+ break;
+ }
+
+ case 't': {
+ const datetime = GLib.DateTime.new_now_local();
+ const datestr = datetime.format('%0X');
+ const datestrEscaped = datestr.replace(/\//g, ':');
+
+ filename += datestrEscaped;
+ break;
+ }
+
+ default:
+ log(`Warning: Unknown escape ${c}`);
+ }
+
+ escape = false;
+ } else if (c === '%') {
+ escape = true;
+ } else {
+ filename += c;
+ }
+ });
+
+ if (escape)
+ filename += '%';
+
+ return this._getAbsolutePath(filename);
+ }
+
+ ScreencastAsync(params, invocation) {
+ let returnValue = [false, ''];
+
+ if (this._lockdownSettings.get_boolean('disable-save-to-disk')) {
+ invocation.return_value(GLib.Variant.new('(bs)', returnValue));
+ return;
+ }
+
+ const sender = invocation.get_sender();
+
+ if (this._recorders.get(sender)) {
+ invocation.return_value(GLib.Variant.new('(bs)', returnValue));
+ return;
+ }
+
+ const [sessionPath] = this._proxy.CreateSessionSync({});
+
+ const [fileTemplate, options] = params;
+ const [screenWidth, screenHeight] = this._introspectProxy.ScreenSize;
+ const filePath = this._generateFilePath(fileTemplate);
+
+ let recorder;
+
+ try {
+ recorder = new Recorder(
+ sessionPath,
+ 0, 0,
+ screenWidth, screenHeight,
+ fileTemplate,
+ options,
+ invocation,
+ _recorder => this._removeRecorder(sender));
+ } catch (error) {
+ log(`Failed to create recorder: ${error.message}`);
+ invocation.return_value(GLib.Variant.new('(bs)', returnValue));
+ return;
+ }
+
+ this._addRecorder(sender, recorder);
+
+ try {
+ recorder.startRecording(
+ (_, result) => {
+ if (result) {
+ returnValue = [true, filePath];
+ invocation.return_value(GLib.Variant.new('(bs)', returnValue));
+ } else {
+ this._removeRecorder(sender);
+ invocation.return_value(GLib.Variant.new('(bs)', returnValue));
+ }
+
+ });
+ } catch (error) {
+ log(`Failed to start recorder: ${error.message}`);
+ this._removeRecorder(sender);
+ invocation.return_value(GLib.Variant.new('(bs)', returnValue));
+ }
+ }
+
+ ScreencastAreaAsync(params, invocation) {
+ let returnValue = [false, ''];
+
+ if (this._lockdownSettings.get_boolean('disable-save-to-disk')) {
+ invocation.return_value(GLib.Variant.new('(bs)', returnValue));
+ return;
+ }
+
+ const sender = invocation.get_sender();
+
+ if (this._recorders.get(sender)) {
+ invocation.return_value(GLib.Variant.new('(bs)', returnValue));
+ return;
+ }
+
+ const [sessionPath] = this._proxy.CreateSessionSync({});
+
+ const [x, y, width, height, fileTemplate, options] = params;
+ const filePath = this._generateFilePath(fileTemplate);
+
+ let recorder;
+
+ try {
+ recorder = new Recorder(
+ sessionPath,
+ x, y,
+ width, height,
+ filePath,
+ options,
+ invocation,
+ _recorder => this._removeRecorder(sender));
+ } catch (error) {
+ log(`Failed to create recorder: ${error.message}`);
+ invocation.return_value(GLib.Variant.new('(bs)', returnValue));
+ return;
+ }
+
+ this._addRecorder(sender, recorder);
+
+ try {
+ recorder.startRecording(
+ (_, result) => {
+ if (result) {
+ returnValue = [true, filePath];
+ invocation.return_value(GLib.Variant.new('(bs)', returnValue));
+ } else {
+ this._removeRecorder(sender);
+ invocation.return_value(GLib.Variant.new('(bs)', returnValue));
+ }
+
+ });
+ } catch (error) {
+ log(`Failed to start recorder: ${error.message}`);
+ this._removeRecorder(sender);
+ invocation.return_value(GLib.Variant.new('(bs)', returnValue));
+ }
+ }
+
+ StopScreencastAsync(params, invocation) {
+ const sender = invocation.get_sender();
+
+ const recorder = this._recorders.get(sender);
+ if (!recorder) {
+ invocation.return_value(GLib.Variant.new('(b)', [false]));
+ return;
+ }
+
+ recorder.stopRecording(() => {
+ this._removeRecorder(sender);
+ invocation.return_value(GLib.Variant.new('(b)', [true]));
+ });
+ }
+};
diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml
index 2289a2dde..07e134353 100644
--- a/js/js-resources.gresource.xml
+++ b/js/js-resources.gresource.xml
@@ -92,7 +92,6 @@
ui/ripples.js
ui/runDialog.js
ui/screenShield.js
- ui/screencast.js
ui/screenshot.js
ui/scripting.js
ui/search.js
@@ -137,7 +136,6 @@
ui/status/volume.js
ui/status/bluetooth.js
ui/status/remoteAccess.js
- ui/status/screencast.js
ui/status/system.js
ui/status/thunderbolt.js
diff --git a/js/misc/fileUtils.js b/js/misc/fileUtils.js
index e672f68bd..2f1798b84 100644
--- a/js/misc/fileUtils.js
+++ b/js/misc/fileUtils.js
@@ -1,6 +1,6 @@
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
/* exported collectFromDatadirs, recursivelyDeleteDir,
- recursivelyMoveDir, loadInterfaceXML */
+ recursivelyMoveDir, loadInterfaceXML, loadSubInterfaceXML */
const { Gio, GLib } = imports.gi;
const Config = imports.misc.config;
@@ -67,14 +67,19 @@ function recursivelyMoveDir(srcDir, destDir) {
}
let _ifaceResource = null;
+function ensureIfaceResource() {
+ if (_ifaceResource)
+ return;
+
+ // don't use global.datadir so the method is usable from tests/tools
+ let dir = GLib.getenv('GNOME_SHELL_DATADIR') || Config.PKGDATADIR;
+ let path = `${dir}/gnome-shell-dbus-interfaces.gresource`;
+ _ifaceResource = Gio.Resource.load(path);
+ _ifaceResource._register();
+}
+
function loadInterfaceXML(iface) {
- if (!_ifaceResource) {
- // don't use global.datadir so the method is usable from tests/tools
- let dir = GLib.getenv('GNOME_SHELL_DATADIR') || Config.PKGDATADIR;
- let path = `${dir}/gnome-shell-dbus-interfaces.gresource`;
- _ifaceResource = Gio.Resource.load(path);
- _ifaceResource._register();
- }
+ ensureIfaceResource();
let uri = `resource:///org/gnome/shell/dbus-interfaces/${iface}.xml`;
let f = Gio.File.new_for_uri(uri);
@@ -88,3 +93,25 @@ function loadInterfaceXML(iface) {
return null;
}
+
+function loadSubInterfaceXML(iface, ifaceFile) {
+ let xml = loadInterfaceXML(ifaceFile);
+ if (!xml)
+ return null;
+
+ let ifaceStartTag = ``;
+ let ifaceStopTag = '';
+ let ifaceStartIndex = xml.indexOf(ifaceStartTag);
+ let ifaceEndIndex = xml.indexOf(ifaceStopTag, ifaceStartIndex + 1) + ifaceStopTag.length;
+
+ let xmlHeader = '\n' +
+ '\n';
+ let xmlFooter = '';
+
+ return (
+ xmlHeader +
+ xml.substr(ifaceStartIndex, ifaceEndIndex - ifaceStartIndex) +
+ xmlFooter);
+}
diff --git a/js/ui/main.js b/js/ui/main.js
index 99ea5201b..bff730c5b 100644
--- a/js/ui/main.js
+++ b/js/ui/main.js
@@ -3,10 +3,10 @@
ctrlAltTabManager, padOsdService, osdWindowManager,
osdMonitorLabeler, shellMountOpDBusService, shellDBusService,
shellAccessDialogDBusService, shellAudioSelectionDBusService,
- screenSaverDBus, screencastService, uiGroup, magnifier,
- xdndHandler, keyboard, kbdA11yDialog, introspectService,
- start, pushModal, popModal, activateWindow, createLookingGlass,
- initializeDeferredWork, getThemeStylesheet, setThemeStylesheet */
+ screenSaverDBus, uiGroup, magnifier, xdndHandler, keyboard,
+ kbdA11yDialog, introspectService, start, pushModal, popModal,
+ activateWindow, createLookingGlass, initializeDeferredWork,
+ getThemeStylesheet, setThemeStylesheet */
const { Clutter, Gio, GLib, GObject, Meta, Shell, St } = imports.gi;
@@ -34,7 +34,6 @@ const LoginManager = imports.misc.loginManager;
const LookingGlass = imports.ui.lookingGlass;
const NotificationDaemon = imports.ui.notificationDaemon;
const WindowAttentionHandler = imports.ui.windowAttentionHandler;
-const Screencast = imports.ui.screencast;
const ScreenShield = imports.ui.screenShield;
const Scripting = imports.ui.scripting;
const SessionMode = imports.ui.sessionMode;
@@ -74,7 +73,6 @@ var shellAudioSelectionDBusService = null;
var shellDBusService = null;
var shellMountOpDBusService = null;
var screenSaverDBus = null;
-var screencastService = null;
var modalCount = 0;
var actionMode = Shell.ActionMode.NONE;
var modalActorFocusStack = [];
@@ -200,7 +198,6 @@ function _initializeUI() {
uiGroup = layoutManager.uiGroup;
padOsdService = new PadOsd.PadOsdService();
- screencastService = new Screencast.ScreencastService();
xdndHandler = new XdndHandler.XdndHandler();
ctrlAltTabManager = new CtrlAltTab.CtrlAltTabManager();
osdWindowManager = new OsdWindow.OsdWindowManager();
diff --git a/js/ui/panel.js b/js/ui/panel.js
index 90aea8dad..4761903f3 100644
--- a/js/ui/panel.js
+++ b/js/ui/panel.js
@@ -736,13 +736,11 @@ class AggregateMenu extends PanelMenu.Button {
this._volume = new imports.ui.status.volume.Indicator();
this._brightness = new imports.ui.status.brightness.Indicator();
this._system = new imports.ui.status.system.Indicator();
- this._screencast = new imports.ui.status.screencast.Indicator();
this._location = new imports.ui.status.location.Indicator();
this._nightLight = new imports.ui.status.nightLight.Indicator();
this._thunderbolt = new imports.ui.status.thunderbolt.Indicator();
this._indicators.add_child(this._thunderbolt);
- this._indicators.add_child(this._screencast);
this._indicators.add_child(this._location);
this._indicators.add_child(this._nightLight);
if (this._network)
diff --git a/js/ui/screencast.js b/js/ui/screencast.js
deleted file mode 100644
index 0b0b14a8e..000000000
--- a/js/ui/screencast.js
+++ /dev/null
@@ -1,146 +0,0 @@
-// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
-
-const { Gio, GLib, Shell } = imports.gi;
-const Signals = imports.signals;
-
-const Main = imports.ui.main;
-
-const { loadInterfaceXML } = imports.misc.fileUtils;
-
-const ScreencastIface = loadInterfaceXML('org.gnome.Shell.Screencast');
-
-var ScreencastService = class {
- constructor() {
- this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(ScreencastIface, this);
- this._dbusImpl.export(Gio.DBus.session, '/org/gnome/Shell/Screencast');
-
- Gio.DBus.session.own_name('org.gnome.Shell.Screencast', Gio.BusNameOwnerFlags.REPLACE, null, null);
-
- this._recorders = new Map();
-
- this._lockdownSettings = new Gio.Settings({ schema_id: 'org.gnome.desktop.lockdown' });
-
- Main.sessionMode.connect('updated', this._sessionUpdated.bind(this));
- }
-
- get isRecording() {
- return this._recorders.size > 0;
- }
-
- _ensureRecorderForSender(sender) {
- let recorder = this._recorders.get(sender);
- if (!recorder) {
- recorder = new Shell.Recorder({ stage: global.stage,
- display: global.display });
- recorder._watchNameId =
- Gio.bus_watch_name(Gio.BusType.SESSION, sender, 0, null,
- this._onNameVanished.bind(this));
- this._recorders.set(sender, recorder);
- this.emit('updated');
- }
- return recorder;
- }
-
- _sessionUpdated() {
- if (Main.sessionMode.allowScreencast)
- return;
-
- for (let sender of this._recorders.keys())
- this._stopRecordingForSender(sender);
- }
-
- _onNameVanished(connection, name) {
- this._stopRecordingForSender(name);
- }
-
- _stopRecordingForSender(sender) {
- let recorder = this._recorders.get(sender);
- if (!recorder)
- return false;
-
- Gio.bus_unwatch_name(recorder._watchNameId);
- recorder.close();
- this._recorders.delete(sender);
- this.emit('updated');
-
- return true;
- }
-
- _applyOptionalParameters(recorder, options) {
- for (let option in options)
- options[option] = options[option].deep_unpack();
-
- if (options['pipeline'])
- recorder.set_pipeline(options['pipeline']);
- if (options['framerate'])
- recorder.set_framerate(options['framerate']);
- if ('draw-cursor' in options)
- recorder.set_draw_cursor(options['draw-cursor']);
- }
-
- ScreencastAsync(params, invocation) {
- let returnValue = [false, ''];
- if (!Main.sessionMode.allowScreencast ||
- this._lockdownSettings.get_boolean('disable-save-to-disk')) {
- invocation.return_value(GLib.Variant.new('(bs)', returnValue));
- return;
- }
-
- let sender = invocation.get_sender();
- let recorder = this._ensureRecorderForSender(sender);
- if (!recorder.is_recording()) {
- let [fileTemplate, options] = params;
-
- recorder.set_file_template(fileTemplate);
- this._applyOptionalParameters(recorder, options);
- let [success, fileName] = recorder.record();
- returnValue = [success, fileName ? fileName : ''];
- if (!success)
- this._stopRecordingForSender(sender);
- }
-
- invocation.return_value(GLib.Variant.new('(bs)', returnValue));
- }
-
- ScreencastAreaAsync(params, invocation) {
- let returnValue = [false, ''];
- if (!Main.sessionMode.allowScreencast ||
- this._lockdownSettings.get_boolean('disable-save-to-disk')) {
- invocation.return_value(GLib.Variant.new('(bs)', returnValue));
- return;
- }
-
- let sender = invocation.get_sender();
- let recorder = this._ensureRecorderForSender(sender);
-
- if (!recorder.is_recording()) {
- let [x, y, width, height, fileTemplate, options] = params;
-
- if (x < 0 || y < 0 ||
- width <= 0 || height <= 0 ||
- x + width > global.screen_width ||
- y + height > global.screen_height) {
- invocation.return_error_literal(Gio.IOErrorEnum,
- Gio.IOErrorEnum.CANCELLED,
- "Invalid params");
- return;
- }
-
- recorder.set_file_template(fileTemplate);
- recorder.set_area(x, y, width, height);
- this._applyOptionalParameters(recorder, options);
- let [success, fileName] = recorder.record();
- returnValue = [success, fileName ? fileName : ''];
- if (!success)
- this._stopRecordingForSender(sender);
- }
-
- invocation.return_value(GLib.Variant.new('(bs)', returnValue));
- }
-
- StopScreencastAsync(params, invocation) {
- let success = this._stopRecordingForSender(invocation.get_sender());
- invocation.return_value(GLib.Variant.new('(b)', [success]));
- }
-};
-Signals.addSignalMethods(ScreencastService.prototype);
diff --git a/js/ui/status/screencast.js b/js/ui/status/screencast.js
deleted file mode 100644
index 6bc162a8f..000000000
--- a/js/ui/status/screencast.js
+++ /dev/null
@@ -1,25 +0,0 @@
-// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
-/* exported Indicator */
-
-const GObject = imports.gi.GObject;
-
-const Main = imports.ui.main;
-const PanelMenu = imports.ui.panelMenu;
-
-var Indicator = GObject.registerClass(
-class Indicator extends PanelMenu.SystemIndicator {
- _init() {
- super._init();
-
- this._indicator = this._addIndicator();
- this._indicator.icon_name = 'media-record-symbolic';
- this._indicator.add_style_class_name('screencast-indicator');
- this._sync();
-
- Main.screencastService.connect('updated', this._sync.bind(this));
- }
-
- _sync() {
- this._indicator.visible = Main.screencastService.isRecording;
- }
-});
diff --git a/meson.build b/meson.build
index 3c5050229..20ee6035a 100644
--- a/meson.build
+++ b/meson.build
@@ -96,9 +96,10 @@ gnome_desktop_dep = dependency('gnome-desktop-3.0', version: gnome_desktop_req)
bt_dep = dependency('gnome-bluetooth-1.0', version: bt_req, required: false)
gst_dep = dependency('gstreamer-1.0', version: gst_req, required: false)
gst_base_dep = dependency('gstreamer-base-1.0', required: false)
+pipewire_dep = dependency('libpipewire-0.3', required: false)
recorder_deps = []
-enable_recorder = gst_dep.found() and gst_base_dep.found()
+enable_recorder = gst_dep.found() and gst_base_dep.found() and pipewire_dep.found()
if enable_recorder
recorder_deps += [gst_dep, gst_base_dep, gtk_dep, x11_dep]
endif
diff --git a/src/meson.build b/src/meson.build
index 546d3a64b..a7c56bbcf 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -163,15 +163,6 @@ libshell_private_sources = [
'shell-app-cache.c',
]
-if enable_recorder
- libshell_sources += ['shell-recorder.c']
- libshell_public_headers += ['shell-recorder.h']
-
- libshell_private_sources += ['shell-recorder-src.c']
- libshell_private_headers += ['shell-recorder-src.h']
-endif
-
-
libshell_enums = gnome.mkenums_simple('shell-enum-types',
sources: libshell_public_headers
)
diff --git a/src/shell-recorder-src.c b/src/shell-recorder-src.c
deleted file mode 100644
index 617e3c0c4..000000000
--- a/src/shell-recorder-src.c
+++ /dev/null
@@ -1,425 +0,0 @@
-/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
-
-#include "config.h"
-
-#define GST_USE_UNSTABLE_API
-#include
-
-#include "shell-recorder-src.h"
-
-struct _ShellRecorderSrc
-{
- GstPushSrc parent;
-
- GMutex mutex;
-
- GstCaps *caps;
- GMutex queue_lock;
- GCond queue_cond;
- GQueue *queue;
-
- gboolean eos;
- gboolean flushing;
- guint memory_used;
- guint memory_used_update_idle;
-};
-
-struct _ShellRecorderSrcClass
-{
- GstPushSrcClass parent_class;
-};
-
-enum {
- PROP_0,
- PROP_CAPS,
- PROP_MEMORY_USED
-};
-
-#define shell_recorder_src_parent_class parent_class
-G_DEFINE_TYPE(ShellRecorderSrc, shell_recorder_src, GST_TYPE_PUSH_SRC);
-
-static void
-shell_recorder_src_init (ShellRecorderSrc *src)
-{
- gst_base_src_set_format (GST_BASE_SRC (src), GST_FORMAT_TIME);
- gst_base_src_set_live (GST_BASE_SRC (src), TRUE);
-
- src->queue = g_queue_new ();
- g_mutex_init (&src->mutex);
- g_mutex_init (&src->queue_lock);
- g_cond_init (&src->queue_cond);
-}
-
-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_source_set_name_by_id (src->memory_used_update_idle, "[gnome-shell] shell_recorder_src_memory_used_update_idle");
- }
- g_mutex_unlock (&src->mutex);
-}
-
-/* _negotiate() is called when we have to decide on a format. We
- * use the configured format */
-static gboolean
-shell_recorder_src_negotiate (GstBaseSrc * base_src)
-{
- ShellRecorderSrc *src = SHELL_RECORDER_SRC (base_src);
- gboolean result;
-
- result = gst_base_src_set_caps (base_src, src->caps);
-
- return result;
-}
-
-static gboolean
-shell_recorder_src_unlock (GstBaseSrc * base_src)
-{
- ShellRecorderSrc *src = SHELL_RECORDER_SRC (base_src);
-
- g_mutex_lock (&src->queue_lock);
- src->flushing = TRUE;
- g_cond_signal (&src->queue_cond);
- g_mutex_unlock (&src->queue_lock);
-
- return TRUE;
-}
-
-static gboolean
-shell_recorder_src_unlock_stop (GstBaseSrc * base_src)
-{
- ShellRecorderSrc *src = SHELL_RECORDER_SRC (base_src);
-
- g_mutex_lock (&src->queue_lock);
- src->flushing = FALSE;
- g_cond_signal (&src->queue_cond);
- g_mutex_unlock (&src->queue_lock);
-
- return TRUE;
-}
-
-static gboolean
-shell_recorder_src_start (GstBaseSrc * base_src)
-{
- ShellRecorderSrc *src = SHELL_RECORDER_SRC (base_src);
-
- g_mutex_lock (&src->queue_lock);
- src->flushing = FALSE;
- src->eos = FALSE;
- g_cond_signal (&src->queue_cond);
- g_mutex_unlock (&src->queue_lock);
-
- return TRUE;
-}
-
-static gboolean
-shell_recorder_src_stop (GstBaseSrc * base_src)
-{
- ShellRecorderSrc *src = SHELL_RECORDER_SRC (base_src);
-
- g_mutex_lock (&src->queue_lock);
- src->flushing = TRUE;
- src->eos = FALSE;
- g_queue_foreach (src->queue, (GFunc) gst_buffer_unref, NULL);
- g_queue_clear (src->queue);
- g_cond_signal (&src->queue_cond);
- g_mutex_unlock (&src->queue_lock);
-
- return TRUE;
-}
-
-static gboolean
-shell_recorder_src_send_event (GstElement * element, GstEvent * event)
-{
- ShellRecorderSrc *src = SHELL_RECORDER_SRC (element);
- gboolean res;
-
- if (GST_EVENT_TYPE (event) == GST_EVENT_EOS)
- {
- shell_recorder_src_close (src);
- gst_event_unref (event);
- res = TRUE;
- }
- else
- {
- res = GST_CALL_PARENT_WITH_DEFAULT (GST_ELEMENT_CLASS, send_event, (element,
- event), FALSE);
- }
- return res;
-}
-
-/* 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;
-
- g_mutex_lock (&src->queue_lock);
- while (TRUE) {
- /* int the flushing state we just return FLUSHING */
- if (src->flushing) {
- g_mutex_unlock (&src->queue_lock);
- return GST_FLOW_FLUSHING;
- }
-
- buffer = g_queue_pop_head (src->queue);
-
- /* we have a buffer, exit the loop to handle it */
- if (buffer != NULL)
- break;
-
- /* no buffer, check EOS */
- if (src->eos) {
- g_mutex_unlock (&src->queue_lock);
- return GST_FLOW_EOS;
- }
- /* wait for something to happen and try again */
- g_cond_wait (&src->queue_cond, &src->queue_lock);
- }
- g_mutex_unlock (&src->queue_lock);
-
- shell_recorder_src_update_memory_used (src,
- - (int)(gst_buffer_get_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);
-
- g_clear_handle_id (&src->memory_used_update_idle, g_source_remove);
-
- shell_recorder_src_set_caps (src, NULL);
- g_queue_free_full (src->queue, (GDestroyNotify) gst_buffer_unref);
-
- g_mutex_clear (&src->mutex);
- g_mutex_clear (&src->queue_lock);
- g_cond_clear (&src->queue_cond);
-
- G_OBJECT_CLASS (shell_recorder_src_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);
- GstBaseSrcClass *base_src_class = GST_BASE_SRC_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;
-
- 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_PARAM_STATIC_STRINGS));
- 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 | G_PARAM_STATIC_STRINGS));
- 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 ");
-
- element_class->send_event = shell_recorder_src_send_event;
-
- base_src_class->negotiate = shell_recorder_src_negotiate;
- base_src_class->unlock = shell_recorder_src_unlock;
- base_src_class->unlock_stop = shell_recorder_src_unlock_stop;
- base_src_class->start = shell_recorder_src_start;
- base_src_class->stop = shell_recorder_src_stop;
-
- push_src_class->create = shell_recorder_src_create;
-}
-
-/**
- * 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);
-
- shell_recorder_src_update_memory_used (src,
- (int)(gst_buffer_get_size(buffer) / 1024));
-
- g_mutex_lock (&src->queue_lock);
- g_queue_push_tail (src->queue, gst_buffer_ref (buffer));
- g_cond_signal (&src->queue_cond);
- g_mutex_unlock (&src->queue_lock);
-}
-
-/**
- * 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 mark ourselves EOS, which will
- * make us send an event once everything has been pushed.
- */
- g_mutex_lock (&src->queue_lock);
- src->eos = TRUE;
- g_cond_signal (&src->queue_cond);
- g_mutex_unlock (&src->queue_lock);
-}
-
-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
deleted file mode 100644
index 6f99c55da..000000000
--- a/src/shell-recorder-src.h
+++ /dev/null
@@ -1,40 +0,0 @@
-/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
-#ifndef __SHELL_RECORDER_SRC_H__
-#define __SHELL_RECORDER_SRC_H__
-
-#include
-
-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
deleted file mode 100644
index 231eab82c..000000000
--- a/src/shell-recorder.c
+++ /dev/null
@@ -1,1625 +0,0 @@
-/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
-
-#include "config.h"
-
-#include
-#include
-#include
-#include
-#include
-
-#define GST_USE_UNSTABLE_API
-#include
-
-#include
-
-#include
-#include
-#include
-#include
-#include
-
-#include "shell-global.h"
-#include "shell-recorder-src.h"
-#include "shell-recorder.h"
-#include "shell-util.h"
-
-typedef enum {
- RECORDER_STATE_CLOSED,
- RECORDER_STATE_RECORDING
-} RecorderState;
-
-typedef struct _RecorderPipeline RecorderPipeline;
-
-struct _ShellRecorder {
- GObject parent;
-
- /* A "maximum" amount of memory to use for buffering. This is used
- * to alert the user that they are filling up memory rather than
- * any that actually affects recording. (In kB)
- */
- guint memory_target;
- guint memory_used; /* Current memory used. (In kB) */
-
- RecorderState state;
-
- ClutterStage *stage;
- gboolean custom_area;
- cairo_rectangle_int_t area;
- int stage_width;
- int stage_height;
-
- int capture_width;
- int capture_height;
- float scale;
-
- int pointer_x;
- int pointer_y;
-
- gboolean draw_cursor;
- MetaCursorTracker *cursor_tracker;
- cairo_surface_t *cursor_image;
- guint8 *cursor_memory;
- int cursor_hot_x;
- int cursor_hot_y;
-
- int framerate;
- char *pipeline_description;
- char *file_template;
-
- /* We might have multiple pipelines that are finishing encoding
- * to go along with the current pipeline where we are recording.
- */
- RecorderPipeline *current_pipeline; /* current pipeline */
- GSList *pipelines; /* all pipelines */
-
- GstClockTime last_frame_time; /* Timestamp for the last frame */
-
- /* GSource IDs for different timeouts and idles */
- guint redraw_timeout;
- guint redraw_idle;
- guint update_memory_used_timeout;
- guint update_pointer_timeout;
- guint repaint_hook_id;
-};
-
-struct _RecorderPipeline
-{
- ShellRecorder *recorder;
- GstElement *pipeline;
- GstElement *src;
- int outfile;
- char *filename;
-};
-
-static void recorder_set_stage (ShellRecorder *recorder,
- ClutterStage *stage);
-static void recorder_set_framerate (ShellRecorder *recorder,
- int framerate);
-static void recorder_set_pipeline (ShellRecorder *recorder,
- const char *pipeline);
-static void recorder_set_file_template (ShellRecorder *recorder,
- const char *file_template);
-static void recorder_set_draw_cursor (ShellRecorder *recorder,
- gboolean draw_cursor);
-
-static void recorder_pipeline_set_caps (RecorderPipeline *pipeline);
-static void recorder_pipeline_closed (RecorderPipeline *pipeline);
-
-static void recorder_remove_redraw_timeout (ShellRecorder *recorder);
-
-enum {
- PROP_0,
- PROP_DISPLAY,
- PROP_STAGE,
- PROP_FRAMERATE,
- PROP_PIPELINE,
- PROP_FILE_TEMPLATE,
- PROP_DRAW_CURSOR
-};
-
-G_DEFINE_TYPE(ShellRecorder, shell_recorder, G_TYPE_OBJECT);
-
-/* The default value of the target frame rate; we'll never record more
- * than this many frames per second, though we may record less if the
- * screen isn't being redrawn. 30 is a compromise between smoothness
- * and the size of the recording.
- */
-#define DEFAULT_FRAMES_PER_SECOND 30
-
-/* The time (in milliseconds) between querying the server for the cursor
- * position.
- */
-#define UPDATE_POINTER_TIME 100
-
-/* The time we wait (in milliseconds) before redrawing when the memory used
- * changes.
- */
-#define UPDATE_MEMORY_USED_DELAY 500
-
-/* Maximum time between frames, in milliseconds. If we don't send data
- * for a long period of time, then when we send the next frame, a lot
- * of work can be created for the encoder to do, so we want to force a
- * periodic redraw when nothing happen.
- */
-#define MAXIMUM_PAUSE_TIME 1000
-
-/* The default pipeline.
- */
-#define DEFAULT_PIPELINE "vp8enc min_quantizer=13 max_quantizer=13 cpu-used=5 deadline=1000000 threads=%T ! queue ! webmmux"
-
-/* If we can find the amount of memory on the machine, we use half
- * of that for memory_target, otherwise, we use this value, in kB.
- */
-#define DEFAULT_MEMORY_TARGET (512*1024)
-
-static guint
-get_memory_target (void)
-{
- FILE *f;
-
- /* Really simple "get amount of memory on the machine" if it
- * doesn't work, you just get the default memory target.
- */
- f = fopen("/proc/meminfo", "r");
- if (!f)
- return DEFAULT_MEMORY_TARGET;
-
- while (!feof(f))
- {
- gchar line_buffer[1024];
- guint mem_total;
- if (fscanf(f, "MemTotal: %u", &mem_total) == 1)
- {
- fclose(f);
- return mem_total / 2;
- }
- /* Skip to the next line and discard what we read */
- if (fgets(line_buffer, sizeof(line_buffer), f) == NULL)
- break;
- }
-
- fclose(f);
-
- return DEFAULT_MEMORY_TARGET;
-}
-
-/*
- * Used to force full stage redraws during recording to avoid artifacts
- *
- * Note: That this will cause the stage to be repainted on
- * every animation frame even if the frame wouldn't normally cause any new
- * drawing
- */
-static gboolean
-recorder_repaint_hook (gpointer data)
-{
- ClutterActor *stage = data;
- clutter_actor_queue_redraw (stage);
-
- return TRUE;
-}
-
-static void
-shell_recorder_init (ShellRecorder *recorder)
-{
- /* Calling gst_init() is a no-op if GStreamer was previously initialized */
- gst_init (NULL, NULL);
-
- shell_recorder_src_register ();
-
- recorder->memory_target = get_memory_target();
-
- recorder->state = RECORDER_STATE_CLOSED;
- recorder->framerate = DEFAULT_FRAMES_PER_SECOND;
- recorder->draw_cursor = TRUE;
-}
-
-static void
-shell_recorder_finalize (GObject *object)
-{
- ShellRecorder *recorder = SHELL_RECORDER (object);
-
- g_clear_handle_id (&recorder->update_memory_used_timeout, g_source_remove);
-
- if (recorder->cursor_image)
- cairo_surface_destroy (recorder->cursor_image);
- if (recorder->cursor_memory)
- g_free (recorder->cursor_memory);
-
- recorder_set_stage (recorder, NULL);
- recorder_set_pipeline (recorder, NULL);
- recorder_set_file_template (recorder, NULL);
-
- recorder_remove_redraw_timeout (recorder);
-
- G_OBJECT_CLASS (shell_recorder_parent_class)->finalize (object);
-}
-
-static void
-recorder_on_stage_destroy (ClutterActor *actor,
- ShellRecorder *recorder)
-{
- recorder_set_stage (recorder, NULL);
-}
-
-/* Add together the memory used by all pipelines; both the
- * currently recording pipeline and pipelines finishing
- * recording asynchronously.
- */
-static void
-recorder_update_memory_used (ShellRecorder *recorder,
- gboolean repaint)
-{
- guint memory_used = 0;
- GSList *l;
-
- for (l = recorder->pipelines; l; l = l->next)
- {
- RecorderPipeline *pipeline = l->data;
- guint pipeline_memory_used;
-
- g_object_get (pipeline->src,
- "memory-used", &pipeline_memory_used,
- NULL);
- memory_used += pipeline_memory_used;
- }
-
- if (memory_used != recorder->memory_used)
- recorder->memory_used = memory_used;
-}
-
-/* Timeout used to avoid not drawing for more than MAXIMUM_PAUSE_TIME
- */
-static gboolean
-recorder_redraw_timeout (gpointer data)
-{
- ShellRecorder *recorder = data;
-
- recorder->redraw_timeout = 0;
- clutter_actor_queue_redraw (CLUTTER_ACTOR (recorder->stage));
-
- return FALSE;
-}
-
-static void
-recorder_add_redraw_timeout (ShellRecorder *recorder)
-{
- if (recorder->redraw_timeout == 0)
- {
- recorder->redraw_timeout = g_timeout_add (MAXIMUM_PAUSE_TIME,
- recorder_redraw_timeout,
- recorder);
- g_source_set_name_by_id (recorder->redraw_timeout, "[gnome-shell] recorder_redraw_timeout");
- }
-}
-
-static void
-recorder_remove_redraw_timeout (ShellRecorder *recorder)
-{
- g_clear_handle_id (&recorder->redraw_timeout, g_source_remove);
-}
-
-static void
-recorder_fetch_cursor_image (ShellRecorder *recorder)
-{
- CoglTexture *texture;
- int width, height;
- int stride;
- guint8 *data;
-
- texture = meta_cursor_tracker_get_sprite (recorder->cursor_tracker);
- if (!texture)
- return;
-
- meta_cursor_tracker_get_hot (recorder->cursor_tracker,
- &recorder->cursor_hot_x, &recorder->cursor_hot_y);
-
- width = cogl_texture_get_width (texture);
- height = cogl_texture_get_height (texture);
- stride = 4 * width;
- data = g_new (guint8, stride * height);
- cogl_texture_get_data (texture, CLUTTER_CAIRO_FORMAT_ARGB32, stride, data);
-
- /* FIXME: cairo-gl? */
- recorder->cursor_image = cairo_image_surface_create_for_data (data,
- CAIRO_FORMAT_ARGB32,
- width, height,
- stride);
- recorder->cursor_memory = data;
-}
-
-/* Overlay the cursor image on the frame. We draw the cursor image
- * into the host-memory buffer after we've captured the frame. An
- * alternate approach would be to turn off the cursor while recording
- * and draw the cursor ourselves with GL, but then we'd need to figure
- * out what the cursor looks like, or hard-code a non-system cursor.
- */
-static void
-recorder_draw_cursor (ShellRecorder *recorder,
- GstBuffer *buffer)
-{
- GstMapInfo info;
- cairo_surface_t *surface;
- cairo_t *cr;
-
- /* We don't show a cursor unless the hot spot is in the frame; this
- * means that sometimes we aren't going to draw a cursor even when
- * there is a little bit overlapping within the stage */
- if (recorder->pointer_x < recorder->area.x ||
- recorder->pointer_y < recorder->area.y ||
- recorder->pointer_x >= recorder->area.x + recorder->area.width ||
- recorder->pointer_y >= recorder->area.y + recorder->area.height)
- return;
-
- if (!recorder->cursor_image)
- recorder_fetch_cursor_image (recorder);
-
- if (!recorder->cursor_image)
- return;
-
- gst_buffer_map (buffer, &info, GST_MAP_WRITE);
- surface = cairo_image_surface_create_for_data (info.data,
- CAIRO_FORMAT_ARGB32,
- recorder->area.width,
- recorder->area.height,
- recorder->area.width * 4);
-
- cr = cairo_create (surface);
- cairo_set_source_surface (cr,
- recorder->cursor_image,
- recorder->pointer_x - recorder->cursor_hot_x - recorder->area.x,
- recorder->pointer_y - recorder->cursor_hot_y - recorder->area.y);
- cairo_paint (cr);
-
- cairo_destroy (cr);
- cairo_surface_destroy (surface);
- gst_buffer_unmap (buffer, &info);
-}
-
-/* Retrieve a frame and feed it into the pipeline
- */
-static void
-recorder_record_frame (ShellRecorder *recorder,
- gboolean paint)
-{
- GstBuffer *buffer;
- ClutterCapture *captures;
- int n_captures;
- cairo_surface_t *image;
- guint size;
- uint8_t *data;
- GstMemory *memory;
- int i;
- GstClock *clock;
- GstClockTime now, base_time;
-
- g_return_if_fail (recorder->current_pipeline != NULL);
-
- /* If we get into the red zone, stop buffering new frames; 13/16 is
- * a bit more than the 3/4 threshold for a red indicator to keep the
- * indicator from flashing between red and yellow. */
- if (recorder->memory_used > (recorder->memory_target * 13) / 16)
- return;
-
- /* Drop frames to get down to something like the target frame rate; since frames
- * are generated with VBlank sync, we don't have full control anyways, so we just
- * drop frames if the interval since the last frame is less than 75% of the
- * desired inter-frame interval.
- */
- clock = gst_element_get_clock (recorder->current_pipeline->src);
-
- /* If we have no clock yet, the pipeline is not yet in PLAYING */
- if (!clock)
- return;
-
- base_time = gst_element_get_base_time (recorder->current_pipeline->src);
- now = gst_clock_get_time (clock) - base_time;
- gst_object_unref (clock);
-
- if (GST_CLOCK_TIME_IS_VALID (recorder->last_frame_time) &&
- now - recorder->last_frame_time < gst_util_uint64_scale_int (GST_SECOND, 3, 4 * recorder->framerate))
- return;
- recorder->last_frame_time = now;
-
- if (!clutter_stage_capture (recorder->stage, paint, &recorder->area,
- &captures, &n_captures))
- return;
-
- if (n_captures == 1)
- image = cairo_surface_reference (captures[0].image);
- else
- image = shell_util_composite_capture_images (captures,
- n_captures,
- recorder->area.x,
- recorder->area.y,
- recorder->capture_width,
- recorder->capture_height,
- recorder->scale);
-
- data = cairo_image_surface_get_data (image);
- size = (cairo_image_surface_get_height (image) *
- cairo_image_surface_get_stride (image));
-
- for (i = 0; i < n_captures; i++)
- cairo_surface_destroy (captures[i].image);
- g_free (captures);
-
- buffer = gst_buffer_new();
- memory = gst_memory_new_wrapped (0, data, size, 0, size,
- image,
- (GDestroyNotify) cairo_surface_destroy);
- gst_buffer_insert_memory (buffer, -1, memory);
-
- GST_BUFFER_PTS(buffer) = now;
-
- if (recorder->draw_cursor)
- {
- StSettings *settings = st_settings_get ();
- gboolean magnifier_active = FALSE;
-
- g_object_get (settings, "magnifier-active", &magnifier_active, NULL);
-
- if (!magnifier_active)
- 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,
- ClutterPaintContext *paint_context,
- ShellRecorder *recorder)
-{
- if (recorder->state == RECORDER_STATE_RECORDING)
- recorder_record_frame (recorder, FALSE);
-}
-
-static void
-recorder_update_size (ShellRecorder *recorder)
-{
- ClutterActorBox allocation;
-
- clutter_actor_get_allocation_box (CLUTTER_ACTOR (recorder->stage), &allocation);
- recorder->stage_width = (int)(0.5 + allocation.x2 - allocation.x1);
- recorder->stage_height = (int)(0.5 + allocation.y2 - allocation.y1);
-
- if (!recorder->custom_area)
- {
- recorder->area.x = 0;
- recorder->area.y = 0;
- recorder->area.width = recorder->stage_width;
- recorder->area.height = recorder->stage_height;
-
- clutter_stage_get_capture_final_size (recorder->stage, NULL,
- &recorder->capture_width,
- &recorder->capture_height,
- &recorder->scale);
- }
-}
-
-static void
-recorder_on_stage_notify_size (ShellRecorder *recorder)
-{
- recorder_update_size (recorder);
-
- /* This breaks the recording but tweaking the GStreamer pipeline a bit
- * might make it work, at least if the codec can handle a stream where
- * the frame size changes in the middle.
- */
- if (recorder->current_pipeline)
- recorder_pipeline_set_caps (recorder->current_pipeline);
-}
-
-static gboolean
-recorder_idle_redraw (gpointer data)
-{
- ShellRecorder *recorder = data;
-
- recorder->redraw_idle = 0;
- clutter_actor_queue_redraw (CLUTTER_ACTOR (recorder->stage));
-
- return FALSE;
-}
-
-static void
-recorder_queue_redraw (ShellRecorder *recorder)
-{
- /* If we just queue a redraw on every mouse motion (for example), we
- * starve Clutter, which operates at a very low priority. So
- * we need to queue a "low priority redraw" after timeline updates
- */
- if (recorder->state == RECORDER_STATE_RECORDING && recorder->redraw_idle == 0)
- {
- recorder->redraw_idle = g_idle_add_full (CLUTTER_PRIORITY_REDRAW + 1,
- recorder_idle_redraw, recorder, NULL);
- g_source_set_name_by_id (recorder->redraw_idle, "[gnome-shell] recorder_idle_redraw");
- }
-}
-
-static void
-on_cursor_changed (MetaCursorTracker *tracker,
- ShellRecorder *recorder)
-{
- if (recorder->cursor_image)
- {
- cairo_surface_destroy (recorder->cursor_image);
- recorder->cursor_image = NULL;
- }
- if (recorder->cursor_memory)
- {
- g_free (recorder->cursor_memory);
- recorder->cursor_memory = NULL;
- }
-
- recorder_queue_redraw (recorder);
-}
-
-static void
-recorder_update_pointer (ShellRecorder *recorder)
-{
- int pointer_x, pointer_y;
-
- meta_cursor_tracker_get_pointer (recorder->cursor_tracker, &pointer_x, &pointer_y, NULL);
-
- if (pointer_x != recorder->pointer_x || pointer_y != recorder->pointer_y)
- {
- recorder->pointer_x = pointer_x;
- recorder->pointer_y = pointer_y;
- recorder_queue_redraw (recorder);
- }
-}
-
-static gboolean
-recorder_update_pointer_timeout (gpointer data)
-{
- recorder_update_pointer (data);
-
- return TRUE;
-}
-
-static void
-recorder_add_update_pointer_timeout (ShellRecorder *recorder)
-{
- if (!recorder->update_pointer_timeout)
- {
- recorder->update_pointer_timeout = g_timeout_add (UPDATE_POINTER_TIME,
- recorder_update_pointer_timeout,
- recorder);
- g_source_set_name_by_id (recorder->update_pointer_timeout, "[gnome-shell] recorder_update_pointer_timeout");
- }
-}
-
-static void
-recorder_remove_update_pointer_timeout (ShellRecorder *recorder)
-{
- g_clear_handle_id (&recorder->update_pointer_timeout, g_source_remove);
-}
-
-static void
-recorder_connect_stage_callbacks (ShellRecorder *recorder)
-{
- g_signal_connect (recorder->stage, "destroy",
- G_CALLBACK (recorder_on_stage_destroy), recorder);
- g_signal_connect_after (recorder->stage, "paint",
- G_CALLBACK (recorder_on_stage_paint), recorder);
- g_signal_connect_swapped (recorder->stage, "notify::width",
- G_CALLBACK (recorder_on_stage_notify_size),
- recorder);
- g_signal_connect_swapped (recorder->stage, "notify::height",
- G_CALLBACK (recorder_on_stage_notify_size),
- recorder);
- g_signal_connect_swapped (recorder->stage, "resource-scale-changed",
- G_CALLBACK (recorder_on_stage_notify_size),
- recorder);
-}
-
-static void
-recorder_disconnect_stage_callbacks (ShellRecorder *recorder)
-{
- g_signal_handlers_disconnect_by_func (recorder->stage,
- (void *)recorder_on_stage_destroy,
- recorder);
- g_signal_handlers_disconnect_by_func (recorder->stage,
- (void *)recorder_on_stage_paint,
- recorder);
- g_signal_handlers_disconnect_by_func (recorder->stage,
- (void *)recorder_on_stage_notify_size,
- recorder);
-
- /* We don't don't deselect for cursor changes in case someone else just
- * happened to be selecting for cursor events on the same window; sending
- * us the events is close to free in any case.
- */
-
- g_clear_handle_id (&recorder->redraw_idle, g_source_remove);
-}
-
-static void
-recorder_set_stage (ShellRecorder *recorder,
- ClutterStage *stage)
-{
- if (recorder->stage == stage)
- return;
-
- if (recorder->current_pipeline)
- shell_recorder_close (recorder);
-
- if (recorder->stage)
- recorder_disconnect_stage_callbacks (recorder);
-
- recorder->stage = stage;
-
- if (recorder->stage)
- recorder_update_size (recorder);
-}
-
-static void
-recorder_set_display (ShellRecorder *recorder,
- MetaDisplay *display)
-{
- MetaCursorTracker *tracker;
-
- tracker = meta_cursor_tracker_get_for_display (display);
-
- if (tracker == recorder->cursor_tracker)
- return;
-
- recorder->cursor_tracker = tracker;
- g_signal_connect_object (tracker, "cursor-changed",
- G_CALLBACK (on_cursor_changed), recorder, 0);
-}
-
-static void
-recorder_set_framerate (ShellRecorder *recorder,
- int framerate)
-{
- if (framerate == recorder->framerate)
- return;
-
- if (recorder->current_pipeline)
- shell_recorder_close (recorder);
-
- recorder->framerate = framerate;
-
- g_object_notify (G_OBJECT (recorder), "framerate");
-}
-
-static void
-recorder_set_pipeline (ShellRecorder *recorder,
- const char *pipeline)
-{
- if (pipeline == recorder->pipeline_description ||
- (pipeline && recorder->pipeline_description && strcmp (recorder->pipeline_description, pipeline) == 0))
- return;
-
- if (recorder->current_pipeline)
- shell_recorder_close (recorder);
-
- if (recorder->pipeline_description)
- g_free (recorder->pipeline_description);
-
- recorder->pipeline_description = g_strdup (pipeline);
-
- g_object_notify (G_OBJECT (recorder), "pipeline");
-}
-
-static void
-recorder_set_file_template (ShellRecorder *recorder,
- const char *file_template)
-{
- if (file_template == recorder->file_template ||
- (file_template && recorder->file_template && strcmp (recorder->file_template, file_template) == 0))
- return;
-
- if (recorder->current_pipeline)
- shell_recorder_close (recorder);
-
- if (recorder->file_template)
- g_free (recorder->file_template);
-
- recorder->file_template = g_strdup (file_template);
-
- g_object_notify (G_OBJECT (recorder), "file-template");
-}
-
-static void
-recorder_set_draw_cursor (ShellRecorder *recorder,
- gboolean draw_cursor)
-{
- if (draw_cursor == recorder->draw_cursor)
- return;
-
- recorder->draw_cursor = draw_cursor;
-
- g_object_notify (G_OBJECT (recorder), "draw-cursor");
-}
-
-static void
-shell_recorder_set_property (GObject *object,
- guint prop_id,
- const GValue *value,
- GParamSpec *pspec)
-{
- ShellRecorder *recorder = SHELL_RECORDER (object);
-
- switch (prop_id)
- {
- case PROP_DISPLAY:
- recorder_set_display (recorder, g_value_get_object (value));
- break;
- case PROP_STAGE:
- recorder_set_stage (recorder, g_value_get_object (value));
- break;
- case PROP_FRAMERATE:
- recorder_set_framerate (recorder, g_value_get_int (value));
- break;
- case PROP_PIPELINE:
- recorder_set_pipeline (recorder, g_value_get_string (value));
- break;
- case PROP_FILE_TEMPLATE:
- recorder_set_file_template (recorder, g_value_get_string (value));
- break;
- case PROP_DRAW_CURSOR:
- recorder_set_draw_cursor (recorder, g_value_get_boolean (value));
- break;
- default:
- G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
- break;
- }
-}
-
-static void
-shell_recorder_get_property (GObject *object,
- guint prop_id,
- GValue *value,
- GParamSpec *pspec)
-{
- ShellRecorder *recorder = SHELL_RECORDER (object);
-
- switch (prop_id)
- {
- case PROP_STAGE:
- g_value_set_object (value, G_OBJECT (recorder->stage));
- break;
- case PROP_FRAMERATE:
- g_value_set_int (value, recorder->framerate);
- break;
- case PROP_PIPELINE:
- g_value_set_string (value, recorder->pipeline_description);
- break;
- case PROP_FILE_TEMPLATE:
- g_value_set_string (value, recorder->file_template);
- break;
- case PROP_DRAW_CURSOR:
- g_value_set_boolean (value, recorder->draw_cursor);
- break;
- default:
- G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
- break;
- }
-}
-
-static void
-shell_recorder_class_init (ShellRecorderClass *klass)
-{
- GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
-
- gobject_class->finalize = shell_recorder_finalize;
- gobject_class->get_property = shell_recorder_get_property;
- gobject_class->set_property = shell_recorder_set_property;
-
- g_object_class_install_property (gobject_class,
- PROP_DISPLAY,
- g_param_spec_object ("display",
- "Display",
- "Display to record",
- META_TYPE_DISPLAY,
- G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
-
- 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_PARAM_STATIC_STRINGS));
-
- g_object_class_install_property (gobject_class,
- PROP_FRAMERATE,
- g_param_spec_int ("framerate",
- "Framerate",
- "Framerate used for resulting video in frames-per-second",
- 0,
- G_MAXINT,
- DEFAULT_FRAMES_PER_SECOND,
- G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
-
- 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_PARAM_STATIC_STRINGS));
-
- g_object_class_install_property (gobject_class,
- PROP_FILE_TEMPLATE,
- g_param_spec_string ("file-template",
- "File Template",
- "The filename template to use for output files",
- NULL,
- G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
-
- g_object_class_install_property (gobject_class,
- PROP_DRAW_CURSOR,
- g_param_spec_boolean ("draw-cursor",
- "Draw Cursor",
- "Whether to record the cursor",
- TRUE,
- G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
-}
-
-/* Sets the GstCaps (video format, in this case) on the stream
- */
-static void
-recorder_pipeline_set_caps (RecorderPipeline *pipeline)
-{
- ShellRecorder *recorder = pipeline->recorder;
- GstCaps *caps;
-
- /* The data is always native-endian xRGB; videoconvert
- * doesn't support little-endian xRGB, but does support
- * big-endian BGRx.
- */
- caps = gst_caps_new_simple ("video/x-raw",
-#if G_BYTE_ORDER == G_LITTLE_ENDIAN
- "format", G_TYPE_STRING, "BGRx",
-#else
- "format", G_TYPE_STRING, "xRGB",
-#endif
- "framerate", GST_TYPE_FRACTION, recorder->framerate, 1,
- "width", G_TYPE_INT, recorder->capture_width,
- "height", G_TYPE_INT, recorder->capture_height,
- NULL);
- g_object_set (pipeline->src, "caps", caps, NULL);
- gst_caps_unref (caps);
-}
-
-/* Augments the supplied pipeline with the source elements: the actual
- * ShellRecorderSrc element where we inject frames then additional elements
- * to convert the output into something palatable.
- */
-static gboolean
-recorder_pipeline_add_source (RecorderPipeline *pipeline)
-{
- GstPad *sink_pad = NULL, *src_pad = NULL;
- gboolean result = FALSE;
- GstElement *videoconvert;
-
- sink_pad = gst_bin_find_unlinked_pad (GST_BIN (pipeline->pipeline), GST_PAD_SINK);
- if (sink_pad == NULL)
- {
- g_warning("ShellRecorder: pipeline has no unlinked sink pad");
- goto out;
- }
-
- pipeline->src = gst_element_factory_make ("shellrecordersrc", NULL);
- if (pipeline->src == NULL)
- {
- g_warning ("Can't create recorder source element");
- goto out;
- }
- gst_bin_add (GST_BIN (pipeline->pipeline), pipeline->src);
-
- recorder_pipeline_set_caps (pipeline);
-
- /* The videoconvert element is a generic converter; it will convert
- * our supplied fixed format data into whatever the encoder wants
- */
- videoconvert = gst_element_factory_make ("videoconvert", NULL);
- if (!videoconvert)
- {
- g_warning("Can't create videoconvert element");
- goto out;
- }
- gst_bin_add (GST_BIN (pipeline->pipeline), videoconvert);
-
- gst_element_link_many (pipeline->src, videoconvert, NULL);
- src_pad = gst_element_get_static_pad (videoconvert, "src");
-
- if (!src_pad)
- {
- g_warning("ShellRecorder: can't get src pad to link into pipeline");
- goto out;
- }
-
- if (gst_pad_link (src_pad, sink_pad) != GST_PAD_LINK_OK)
- {
- g_warning("ShellRecorder: can't link to sink pad");
- goto out;
- }
-
- result = TRUE;
-
- out:
- if (sink_pad)
- gst_object_unref (sink_pad);
- if (src_pad)
- gst_object_unref (src_pad);
-
- return result;
-}
-
-static char *
-get_absolute_path (char *maybe_relative)
-{
- char *path;
-
- if (g_path_is_absolute (maybe_relative))
- path = g_strdup (maybe_relative);
- else
- {
- const char *video_dir = g_get_user_special_dir (G_USER_DIRECTORY_VIDEOS);
- if (!g_file_test (video_dir, G_FILE_TEST_EXISTS))
- video_dir = g_get_home_dir ();
-
- path = g_build_filename (video_dir, maybe_relative, NULL);
- }
-
- return path;
-}
-
-/* Open a file for writing. Opening the file ourselves and using fdsink has
- * the advantage over filesink of being able to use O_EXCL when we want to
- * avoid overwriting* an existing file. Returns -1 if the file couldn't
- * be opened.
- */
-static int
-recorder_open_outfile (ShellRecorder *recorder,
- char **outfilename)
-{
- const char *pattern;
- int flags;
- int outfile = -1;
-
- pattern = recorder->file_template;
- if (!pattern)
- return -1;
-
- while (TRUE)
- {
- GString *filename = g_string_new (NULL);
- const char *p;
- char *path;
-
- for (p = pattern; *p; p++)
- {
- if (*p == '%')
- {
- switch (*(p + 1))
- {
- case '%':
- case '\0':
- g_string_append_c (filename, '%');
- break;
- case 'd':
- {
- /* Appends date according to locale */
- GDateTime *datetime = g_date_time_new_now_local ();
- char *date_str = g_date_time_format (datetime, "%0x");
- char *s;
-
- for (s = date_str; *s; s++)
- if (G_IS_DIR_SEPARATOR (*s))
- *s = '-';
-
- g_string_append (filename, date_str);
- g_free (date_str);
- g_date_time_unref (datetime);
- }
- break;
- case 't':
- {
- /* Appends time according to locale */
- GDateTime *datetime = g_date_time_new_now_local ();
- char *time_str = g_date_time_format (datetime, "%0X");
- char *s;
-
- for (s = time_str; *s; s++)
- if (G_IS_DIR_SEPARATOR (*s))
- *s = ':';
-
- g_string_append (filename, time_str);
- g_free (time_str);
- g_date_time_unref (datetime);
- }
- break;
- default:
- g_warning ("Unknown escape %%%c in filename", *(p + 1));
- goto out;
- }
-
- p++;
- }
- else
- g_string_append_c (filename, *p);
- }
-
- /* If a filename is explicitly specified without %u then we assume the user
- * is fine with over-writing the old contents; putting %u in the default
- * should avoid problems with malicious symlinks.
- */
- flags = O_WRONLY | O_CREAT | O_TRUNC;
-
- path = get_absolute_path (filename->str);
- outfile = open (path, flags, 0666);
- if (outfile != -1)
- {
- g_printerr ("Recording to %s\n", path);
-
- if (outfilename != NULL)
- *outfilename = path;
- else
- g_free (path);
- g_string_free (filename, TRUE);
-
- goto out;
- }
-
- if (outfile == -1 && errno != EEXIST)
- {
- g_warning ("Cannot open output file '%s': %s", path, g_strerror (errno));
- g_string_free (filename, TRUE);
- g_free (path);
- goto out;
- }
-
- g_string_free (filename, TRUE);
- g_free (path);
- }
-
- out:
-
- return outfile;
-}
-
-/* Augments the supplied pipeline with a sink element to write to the output
- * file, if necessary.
- */
-static gboolean
-recorder_pipeline_add_sink (RecorderPipeline *pipeline)
-{
- GstPad *sink_pad = NULL, *src_pad = NULL;
- GstElement *fdsink;
- gboolean result = FALSE;
-
- src_pad = gst_bin_find_unlinked_pad (GST_BIN (pipeline->pipeline), GST_PAD_SRC);
- if (src_pad == NULL)
- {
- /* Nothing to do - assume that we were given a complete pipeline */
- return TRUE;
- }
-
- pipeline->outfile = recorder_open_outfile (pipeline->recorder,
- &pipeline->filename);
- if (pipeline->outfile == -1)
- goto out;
-
- fdsink = gst_element_factory_make ("fdsink", NULL);
- if (fdsink == NULL)
- {
- g_warning("Can't create fdsink element");
- goto out;
- }
- gst_bin_add (GST_BIN (pipeline->pipeline), fdsink);
- g_object_set (fdsink, "fd", pipeline->outfile, NULL);
-
- sink_pad = gst_element_get_static_pad (fdsink, "sink");
- if (!sink_pad)
- {
- g_warning("ShellRecorder: can't get sink pad to link pipeline output");
- goto out;
- }
-
- if (gst_pad_link (src_pad, sink_pad) != GST_PAD_LINK_OK)
- {
- g_warning("ShellRecorder: can't link to sink pad");
- goto out;
- }
-
- result = TRUE;
-
- out:
- if (src_pad)
- gst_object_unref (src_pad);
- if (sink_pad)
- gst_object_unref (sink_pad);
-
- return result;
-}
-
-static gboolean
-recorder_update_memory_used_timeout (gpointer data)
-{
- ShellRecorder *recorder = data;
- recorder->update_memory_used_timeout = 0;
-
- recorder_update_memory_used (recorder, TRUE);
-
- return FALSE;
-}
-
-/* We throttle down the frequency which we recompute memory usage
- * and draw the buffer indicator to avoid cutting into performance.
- */
-static void
-recorder_pipeline_on_memory_used_changed (ShellRecorderSrc *src,
- GParamSpec *spec,
- RecorderPipeline *pipeline)
-{
- ShellRecorder *recorder = pipeline->recorder;
- if (!recorder)
- return;
-
- if (recorder->update_memory_used_timeout == 0)
- {
- recorder->update_memory_used_timeout = g_timeout_add (UPDATE_MEMORY_USED_DELAY,
- recorder_update_memory_used_timeout,
- recorder);
- g_source_set_name_by_id (recorder->update_memory_used_timeout, "[gnome-shell] recorder_update_memory_used_timeout");
- }
-}
-
-static void
-recorder_pipeline_free (RecorderPipeline *pipeline)
-{
- if (pipeline->pipeline != NULL)
- gst_object_unref (pipeline->pipeline);
-
- if (pipeline->outfile != -1)
- close (pipeline->outfile);
-
- g_free (pipeline->filename);
-
- g_clear_object (&pipeline->recorder);
-
- g_free (pipeline);
-}
-
-/* Function gets called on pipeline-global events; we use it to
- * know when the pipeline is finished.
- */
-static gboolean
-recorder_pipeline_bus_watch (GstBus *bus,
- GstMessage *message,
- gpointer data)
-{
- RecorderPipeline *pipeline = data;
-
- if (message->type == GST_MESSAGE_EOS)
- {
- recorder_pipeline_closed (pipeline);
- return FALSE; /* remove watch */
- }
- else if (message->type == GST_MESSAGE_ERROR)
- {
- GError *error;
-
- gst_message_parse_error (message, &error, NULL);
- g_warning ("Error in recording pipeline: %s\n", error->message);
- g_error_free (error);
- recorder_pipeline_closed (pipeline);
- return FALSE; /* remove watch */
- }
-
- /* Leave the watch in place */
- return TRUE;
-}
-
-/* Clean up when the pipeline is finished
- */
-static void
-recorder_pipeline_closed (RecorderPipeline *pipeline)
-{
- g_signal_handlers_disconnect_by_func (pipeline->src,
- (gpointer) recorder_pipeline_on_memory_used_changed,
- pipeline);
-
- recorder_disconnect_stage_callbacks (pipeline->recorder);
-
- gst_element_set_state (pipeline->pipeline, GST_STATE_NULL);
-
- if (pipeline->recorder)
- {
- GtkRecentManager *recent_manager;
- GFile *file;
- char *uri;
-
- ShellRecorder *recorder = pipeline->recorder;
- if (pipeline == recorder->current_pipeline)
- {
- /* Error case; force a close */
- recorder->current_pipeline = NULL;
- shell_recorder_close (recorder);
- }
-
- recent_manager = gtk_recent_manager_get_default ();
-
- file = g_file_new_for_path (pipeline->filename);
- uri = g_file_get_uri (file);
- gtk_recent_manager_add_item (recent_manager,
- uri);
- g_free (uri);
- g_object_unref (file);
-
- recorder->pipelines = g_slist_remove (recorder->pipelines, pipeline);
- }
-
- recorder_pipeline_free (pipeline);
-}
-
-/*
- * Replaces '%T' in the passed pipeline with the thread count,
- * the maximum possible value is 64 (limit of what vp9enc supports)
- *
- * It is assumes that %T occurs only once.
- */
-static char*
-substitute_thread_count (const char *pipeline)
-{
- char *tmp;
- int n_threads;
- GString *result;
-
- tmp = strstr (pipeline, "%T");
-
- if (!tmp)
- return g_strdup (pipeline);
-
-#ifdef _SC_NPROCESSORS_ONLN
- {
- int n_processors = sysconf (_SC_NPROCESSORS_ONLN); /* includes hyper-threading */
- n_threads = MIN (MAX (1, n_processors - 1), 64);
- }
-#else
- n_threads = 3;
-#endif
-
- result = g_string_new (NULL);
- g_string_append_len (result, pipeline, tmp - pipeline);
- g_string_append_printf (result, "%d", n_threads);
- g_string_append (result, tmp + 2);
-
- return g_string_free (result, FALSE);
-}
-
-static gboolean
-recorder_open_pipeline (ShellRecorder *recorder)
-{
- RecorderPipeline *pipeline;
- const char *pipeline_description;
- char *parsed_pipeline;
- GError *error = NULL;
- GstBus *bus;
-
- pipeline = g_new0 (RecorderPipeline, 1);
- pipeline->recorder = g_object_ref (recorder);
- pipeline->outfile = - 1;
-
- pipeline_description = recorder->pipeline_description;
- if (!pipeline_description)
- pipeline_description = DEFAULT_PIPELINE;
-
- parsed_pipeline = substitute_thread_count (pipeline_description);
-
- pipeline->pipeline = gst_parse_launch_full (parsed_pipeline, NULL,
- GST_PARSE_FLAG_FATAL_ERRORS,
- &error);
- g_free (parsed_pipeline);
-
- if (pipeline->pipeline == NULL)
- {
- g_warning ("ShellRecorder: failed to parse pipeline: %s", error->message);
- g_error_free (error);
- goto error;
- }
-
- if (!recorder_pipeline_add_source (pipeline))
- goto error;
-
- if (!recorder_pipeline_add_sink (pipeline))
- goto error;
-
- gst_element_set_state (pipeline->pipeline, GST_STATE_PLAYING);
-
- bus = gst_pipeline_get_bus (GST_PIPELINE (pipeline->pipeline));
- gst_bus_add_watch (bus, recorder_pipeline_bus_watch, pipeline);
- gst_object_unref (bus);
-
- g_signal_connect (pipeline->src, "notify::memory-used",
- G_CALLBACK (recorder_pipeline_on_memory_used_changed), pipeline);
-
- recorder->current_pipeline = pipeline;
- recorder->pipelines = g_slist_prepend (recorder->pipelines, pipeline);
-
- return TRUE;
-
- error:
- recorder_pipeline_free (pipeline);
-
- return FALSE;
-}
-
-static void
-recorder_close_pipeline (ShellRecorder *recorder)
-{
- if (recorder->current_pipeline != NULL)
- {
- /* This will send an EOS (end-of-stream) message after the last frame
- * is written. The bus watch for the pipeline will get it and do
- * final cleanup
- */
- gst_element_send_event (recorder->current_pipeline->pipeline,
- gst_event_new_eos());
- recorder->current_pipeline = NULL;
- }
-}
-
-/**
- * shell_recorder_new:
- * @stage: The #ClutterStage
- *
- * Create a new #ShellRecorder to record movies of a #ClutterStage
- *
- * Return value: The newly created #ShellRecorder object
- */
-ShellRecorder *
-shell_recorder_new (ClutterStage *stage)
-{
- return g_object_new (SHELL_TYPE_RECORDER,
- "stage", stage,
- NULL);
-}
-
-/**
- * shell_recorder_set_framerate:
- * @recorder: the #ShellRecorder
- * @framerate: Framerate used for resulting video in frames-per-second.
- *
- * Sets the number of frames per second we try to record. Less frames
- * will be recorded when the screen doesn't need to be redrawn this
- * quickly. (This value will also be set as the framerate for the
- * GStreamer pipeline; whether that has an effect on the resulting
- * video will depend on the details of the pipeline and the codec. The
- * default encoding to webm format doesn't pay attention to the pipeline
- * framerate.)
- *
- * The default value is 30.
- */
-void
-shell_recorder_set_framerate (ShellRecorder *recorder,
- int framerate)
-{
- g_return_if_fail (SHELL_IS_RECORDER (recorder));
-
- recorder_set_framerate (recorder, framerate);
-}
-
-/**
- * shell_recorder_set_file_template:
- * @recorder: the #ShellRecorder
- * @file_template: the filename template to use for output files,
- * or %NULL for the defalt value.
- *
- * Sets the filename that will be used when creating output
- * files. This is only used if the configured pipeline has an
- * unconnected source pad (as the default pipeline does). If
- * the pipeline is complete, then the filename is unused. The
- * provided string is used as a template.It can contain
- * the following escapes:
- *
- * %d: The current date as YYYYYMMDD
- * %%: A literal percent
- *
- * The default value is 'shell-%d%u-%c.ogg'.
- */
-void
-shell_recorder_set_file_template (ShellRecorder *recorder,
- const char *file_template)
-{
- g_return_if_fail (SHELL_IS_RECORDER (recorder));
-
- recorder_set_file_template (recorder, file_template);
-
-}
-
-void
-shell_recorder_set_draw_cursor (ShellRecorder *recorder,
- gboolean draw_cursor)
-{
- g_return_if_fail (SHELL_IS_RECORDER (recorder));
-
- recorder_set_draw_cursor (recorder, draw_cursor);
-}
-
-/**
- * shell_recorder_set_pipeline:
- * @recorder: the #ShellRecorder
- * @pipeline: (nullable): the GStreamer pipeline used to encode recordings
- * or %NULL for the default value.
- *
- * Sets the GStreamer pipeline used to encode recordings.
- * It follows the syntax used for gst-launch. The pipeline
- * should have an unconnected sink pad where the recorded
- * video is recorded. It will normally have a unconnected
- * source pad; output from that pad will be written into the
- * output file. (See shell_recorder_set_file_template().) However
- * the pipeline can also take care of its own output - this
- * might be used to send the output to an icecast server
- * via shout2send or similar.
- *
- * The default value is 'vp8enc min_quantizer=13 max_quantizer=13 cpu-used=5 deadline=1000000 threads=%T ! queue ! webmmux'
- */
-void
-shell_recorder_set_pipeline (ShellRecorder *recorder,
- const char *pipeline)
-{
- g_return_if_fail (SHELL_IS_RECORDER (recorder));
-
- recorder_set_pipeline (recorder, pipeline);
-}
-
-void
-shell_recorder_set_area (ShellRecorder *recorder,
- int x,
- int y,
- int width,
- int height)
-{
- g_return_if_fail (SHELL_IS_RECORDER (recorder));
-
- recorder->custom_area = TRUE;
- recorder->area.x = CLAMP (x, 0, recorder->stage_width);
- recorder->area.y = CLAMP (y, 0, recorder->stage_height);
- recorder->area.width = CLAMP (width,
- 0, recorder->stage_width - recorder->area.x);
- recorder->area.height = CLAMP (height,
- 0, recorder->stage_height - recorder->area.y);
-
- clutter_stage_get_capture_final_size (recorder->stage, &recorder->area,
- &recorder->capture_width,
- &recorder->capture_height,
- &recorder->scale);
-
- /* This breaks the recording but tweaking the GStreamer pipeline a bit
- * might make it work, at least if the codec can handle a stream where
- * the frame size changes in the middle.
- */
- if (recorder->current_pipeline)
- recorder_pipeline_set_caps (recorder->current_pipeline);
-}
-
-/**
- * shell_recorder_record:
- * @recorder: the #ShellRecorder
- * @filename_used: (out) (optional): actual filename used for recording
- *
- * Starts recording, Starting the recording may fail if the output file
- * cannot be opened, or if the output stream cannot be created
- * for other reasons. In that case a warning is printed to
- * stderr. There is no way currently to get details on how
- * recording failed to start.
- *
- * An extra reference count is added to the recorder if recording
- * is succesfully started; the recording object will not be freed
- * until recording is stopped even if the creator no longer holds
- * a reference. Recording is automatically stopped if the stage
- * is destroyed.
- *
- * Return value: %TRUE if recording was succesfully started
- */
-gboolean
-shell_recorder_record (ShellRecorder *recorder,
- char **filename_used)
-{
- g_return_val_if_fail (SHELL_IS_RECORDER (recorder), FALSE);
- g_return_val_if_fail (recorder->stage != NULL, FALSE);
- g_return_val_if_fail (recorder->state != RECORDER_STATE_RECORDING, FALSE);
-
- if (!recorder_open_pipeline (recorder))
- return FALSE;
-
- if (filename_used)
- *filename_used = g_strdup (recorder->current_pipeline->filename);
-
- recorder_connect_stage_callbacks (recorder);
-
- recorder->last_frame_time = GST_CLOCK_TIME_NONE;
-
- recorder->state = RECORDER_STATE_RECORDING;
- recorder_update_pointer (recorder);
- recorder_add_update_pointer_timeout (recorder);
-
- /* Disable unredirection while we are recoring */
- meta_disable_unredirect_for_display (shell_global_get_display (shell_global_get ()));
-
- /* Set up repaint hook */
- recorder->repaint_hook_id = clutter_threads_add_repaint_func(recorder_repaint_hook, recorder->stage, NULL);
-
- /* Record an initial frame and also redraw with the indicator */
- clutter_actor_queue_redraw (CLUTTER_ACTOR (recorder->stage));
-
- /* We keep a ref while recording to let a caller start a recording then
- * drop their reference to the recorder
- */
- g_object_ref (recorder);
-
- return TRUE;
-}
-
-/**
- * shell_recorder_close:
- * @recorder: the #ShellRecorder
- *
- * Stops recording. It's possible to call shell_recorder_record()
- * again to reopen a new recording stream, but unless change the
- * recording filename, this may result in the old recording being
- * overwritten.
- */
-void
-shell_recorder_close (ShellRecorder *recorder)
-{
- g_return_if_fail (SHELL_IS_RECORDER (recorder));
- g_return_if_fail (recorder->state != RECORDER_STATE_CLOSED);
-
- /* We want to record one more frame since some time may have
- * elapsed since the last frame
- */
- recorder_record_frame (recorder, TRUE);
-
- recorder_remove_update_pointer_timeout (recorder);
- recorder_close_pipeline (recorder);
-
- /* Queue a redraw to remove the recording indicator */
- clutter_actor_queue_redraw (CLUTTER_ACTOR (recorder->stage));
-
- if (recorder->repaint_hook_id != 0)
- {
- clutter_threads_remove_repaint_func (recorder->repaint_hook_id);
- recorder->repaint_hook_id = 0;
- }
-
- recorder->state = RECORDER_STATE_CLOSED;
-
- /* Reenable after the recording */
- meta_enable_unredirect_for_display (shell_global_get_display (shell_global_get ()));
-
- /* Release the refcount we took when we started recording */
- g_object_unref (recorder);
-}
-
-/**
- * shell_recorder_is_recording:
- *
- * Determine if recording is currently in progress. (The recorder
- * is not paused or closed.)
- *
- * Return value: %TRUE if the recorder is currently recording.
- */
-gboolean
-shell_recorder_is_recording (ShellRecorder *recorder)
-{
- g_return_val_if_fail (SHELL_IS_RECORDER (recorder), FALSE);
-
- return recorder->state == RECORDER_STATE_RECORDING;
-}
diff --git a/src/shell-recorder.h b/src/shell-recorder.h
deleted file mode 100644
index c1e0e6368..000000000
--- a/src/shell-recorder.h
+++ /dev/null
@@ -1,45 +0,0 @@
-/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
-#ifndef __SHELL_RECORDER_H__
-#define __SHELL_RECORDER_H__
-
-#include
-
-G_BEGIN_DECLS
-
-/**
- * SECTION:shell-recorder
- * @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.
- */
-#define SHELL_TYPE_RECORDER (shell_recorder_get_type ())
-G_DECLARE_FINAL_TYPE (ShellRecorder, shell_recorder, SHELL, RECORDER, GObject)
-
-ShellRecorder *shell_recorder_new (ClutterStage *stage);
-
-void shell_recorder_set_framerate (ShellRecorder *recorder,
- int framerate);
-void shell_recorder_set_file_template (ShellRecorder *recorder,
- const char *file_template);
-void shell_recorder_set_pipeline (ShellRecorder *recorder,
- const char *pipeline);
-void shell_recorder_set_draw_cursor (ShellRecorder *recorder,
- gboolean draw_cursor);
-void shell_recorder_set_area (ShellRecorder *recorder,
- int x,
- int y,
- int width,
- int height);
-gboolean shell_recorder_record (ShellRecorder *recorder,
- char **filename_used);
-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__ */