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