From 2b0731ab81887fd9767da73695c754c5b9aadd7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20=C3=85dahl?= Date: Thu, 23 Apr 2020 20:46:44 +0200 Subject: [PATCH] Move screencasting into a separate service process Move the screencasting into a separate D-Bus service process, using PipeWire instead of Clutter API. The service is implemented in Javascript using the dbusService.js helper, and implements the same API as was done by screencast.js and the corresponding C code. https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1372 --- .../org.gnome.Mutter.ScreenCast.xml | 191 ++ .../gnome-shell-dbus-interfaces.gresource.xml | 1 + docs/reference/shell/meson.build | 5 - js/dbusServices/meson.build | 6 + ...g.gnome.Shell.Screencast.src.gresource.xml | 11 + js/dbusServices/screencast/main.js | 11 + .../screencast/screencastService.js | 458 +++++ js/js-resources.gresource.xml | 2 - js/misc/fileUtils.js | 43 +- js/ui/main.js | 11 +- js/ui/panel.js | 2 - js/ui/screencast.js | 146 -- js/ui/status/screencast.js | 25 - meson.build | 3 +- src/meson.build | 9 - src/shell-recorder-src.c | 425 ----- src/shell-recorder-src.h | 40 - src/shell-recorder.c | 1625 ----------------- src/shell-recorder.h | 45 - 19 files changed, 719 insertions(+), 2340 deletions(-) create mode 100644 data/dbus-interfaces/org.gnome.Mutter.ScreenCast.xml create mode 100644 js/dbusServices/org.gnome.Shell.Screencast.src.gresource.xml create mode 100644 js/dbusServices/screencast/main.js create mode 100644 js/dbusServices/screencast/screencastService.js delete mode 100644 js/ui/screencast.js delete mode 100644 js/ui/status/screencast.js delete mode 100644 src/shell-recorder-src.c delete mode 100644 src/shell-recorder-src.h delete mode 100644 src/shell-recorder.c delete mode 100644 src/shell-recorder.h 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__ */