2020-04-23 18:46:44 +00:00
|
|
|
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
|
|
|
/* exported ScreencastService */
|
|
|
|
|
Specify API versions for all public GIR APIs, except GLib
If one of these libraries breaks its GIR API in future, then upgrading
packages unrelated to gnome-shell might pull in the newer version,
causing gnome-shell to crash when it gets a newer GIR API that is
incompatible with its expectations. For example, this seems to be
happening in Debian testing at the moment, when GNOME Shell 41.4
imports GWeather and can get version 4.0 instead of the version 3.0 that
it expected.
Adding explicit API versions at the time the newer version is released
is too late, because that will still let the newer version of the GIR API
break pre-existing GNOME Shell packages. Prevent similar crashes in
future by making the desired versions explicit.
This is done for all third-party libraries except GLib, similar to the
common practice in Python code; if GLib breaks API, then that will be
a disruptive change to the whole GLib/GObject ecosystem, regardless.
Gvc, Meta, Shell, Shew, St are not included because they're private
(only exist in a non-default search path entry).
Clutter and Cogl *are* included, because we need to import the fork of
them that comes with Meta, as opposed to their deprecated standalone
versions.
Signed-off-by: Simon McVittie <smcv@debian.org>
Bug-Debian: https://bugs.debian.org/1008926
Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/2261>
2022-04-04 10:26:43 +00:00
|
|
|
imports.gi.versions.Gst = '1.0';
|
2021-01-20 01:48:24 +00:00
|
|
|
imports.gi.versions.Gtk = '4.0';
|
2020-09-14 17:55:17 +00:00
|
|
|
|
|
|
|
const { Gio, GLib, Gst, Gtk } = imports.gi;
|
2020-04-23 18:46:44 +00:00
|
|
|
|
2022-07-05 07:25:02 +00:00
|
|
|
const { loadInterfaceXML, loadSubInterfaceXML } = imports.misc.dbusUtils;
|
2020-04-23 18:46:44 +00:00
|
|
|
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);
|
|
|
|
|
screencastService: Improve the gstreamer pipeline for recording
The current gstreamer pipeline performs quite bad on slower machines and
is dropping lots of frames, improve the pipeline by changing a few
things:
- Use threads for videoconvert and improve speed of videoconvert by
disabling some unneeded things
- Add a queue before the encoding step, this allows the encoder to work
at its own pace and will lead to a lot more stability
- Remove the fixed quantizer and only set a max quantizer, this helps
quite a bit with performance
- Change the deadline parameter of vp8enc to 1: This makes the encoder
go into real time mode, which will make it a lot faster
- Set cpu-used to 16, the maximum possible value.
- Set static-threshold to 1000, static-threshold is the motion detection
threshold, and while a value of 100 is recommended for screencasting in
the gstreamer documentation (see [1]), using 1000 appears to perform a
lot better and still outputs fairly good quality
- Set a larger buffer size than the default size, this seems to get a
bit more stability during high load scenarios
All in all, those changes make the pipeline drop no more frames when
recording at 30 FPS and 2K screen resolution. That was tested on a
fairly recent mobile core-i5 processor.
Also, because we now have two %T replacement strings for the number of
threads, we need to switch to replaceAll(). For that to work, we have to
put the %T matching expression into quotes.
[1] https://gstreamer.freedesktop.org/documentation/vpx/GstVPXEnc.html?gi-language=c#GstVPXEnc:static-threshold
Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1633>
2021-01-26 08:31:49 +00:00
|
|
|
const DEFAULT_PIPELINE = 'videoconvert chroma-mode=GST_VIDEO_CHROMA_MODE_NONE dither=GST_VIDEO_DITHER_NONE matrix-mode=GST_VIDEO_MATRIX_MODE_OUTPUT_ONLY n-threads=%T ! queue ! vp8enc cpu-used=16 max-quantizer=17 deadline=1 keyframe-mode=disabled threads=%T static-threshold=1000 buffer-size=20000 ! queue ! webmmux';
|
2020-04-23 18:46:44 +00:00
|
|
|
const DEFAULT_FRAMERATE = 30;
|
|
|
|
const DEFAULT_DRAW_CURSOR = true;
|
|
|
|
|
|
|
|
const PipelineState = {
|
2022-01-28 23:31:00 +00:00
|
|
|
INIT: 'INIT',
|
|
|
|
PLAYING: 'PLAYING',
|
|
|
|
FLUSHING: 'FLUSHING',
|
|
|
|
STOPPED: 'STOPPED',
|
2022-01-28 23:31:15 +00:00
|
|
|
ERROR: 'ERROR',
|
2020-04-23 18:46:44 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
const SessionState = {
|
2022-01-28 23:31:00 +00:00
|
|
|
INIT: 'INIT',
|
|
|
|
ACTIVE: 'ACTIVE',
|
|
|
|
STOPPED: 'STOPPED',
|
2020-04-23 18:46:44 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
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._x = x;
|
|
|
|
this._y = y;
|
|
|
|
this._width = width;
|
|
|
|
this._height = height;
|
|
|
|
this._filePath = filePath;
|
|
|
|
|
2021-12-17 07:17:35 +00:00
|
|
|
try {
|
|
|
|
const dir = Gio.File.new_for_path(filePath).get_parent();
|
|
|
|
dir.make_directory_with_parents(null);
|
|
|
|
} catch (e) {
|
|
|
|
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.EXISTS))
|
|
|
|
throw e;
|
|
|
|
}
|
|
|
|
|
2020-04-23 18:46:44 +00:00
|
|
|
this._pipelineString = DEFAULT_PIPELINE;
|
|
|
|
this._framerate = DEFAULT_FRAMERATE;
|
|
|
|
this._drawCursor = DEFAULT_DRAW_CURSOR;
|
|
|
|
|
2022-01-28 22:50:35 +00:00
|
|
|
this._pipelineState = PipelineState.INIT;
|
|
|
|
this._pipeline = null;
|
|
|
|
|
2020-04-23 18:46:44 +00:00
|
|
|
this._applyOptions(options);
|
|
|
|
this._watchSender(invocation.get_sender());
|
|
|
|
|
2022-01-28 22:50:35 +00:00
|
|
|
this._sessionState = SessionState.INIT;
|
2020-04-23 18:46:44 +00:00
|
|
|
this._initSession(sessionPath);
|
|
|
|
}
|
|
|
|
|
|
|
|
_applyOptions(options) {
|
|
|
|
for (const option in options)
|
2022-08-10 09:56:14 +00:00
|
|
|
options[option] = options[option].deepUnpack();
|
2020-04-23 18:46:44 +00:00
|
|
|
|
|
|
|
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'];
|
|
|
|
}
|
|
|
|
|
2020-09-14 17:55:17 +00:00
|
|
|
_addRecentItem() {
|
|
|
|
const file = Gio.File.new_for_path(this._filePath);
|
|
|
|
Gtk.RecentManager.get_default().add_item(file.get_uri());
|
|
|
|
}
|
|
|
|
|
2020-04-23 18:46:44 +00:00
|
|
|
_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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-28 22:50:35 +00:00
|
|
|
_teardownPipeline() {
|
|
|
|
if (!this._pipeline)
|
|
|
|
return;
|
|
|
|
|
|
|
|
if (this._pipeline.set_state(Gst.State.NULL) !== Gst.StateChangeReturn.SUCCESS)
|
|
|
|
log('Failed to set pipeline state to NULL');
|
2020-04-23 18:46:44 +00:00
|
|
|
|
2022-01-28 22:50:35 +00:00
|
|
|
this._pipelineState = PipelineState.STOPPED;
|
|
|
|
this._pipeline = null;
|
2020-04-23 18:46:44 +00:00
|
|
|
}
|
|
|
|
|
2022-01-28 22:50:35 +00:00
|
|
|
_stopSession() {
|
|
|
|
if (this._sessionState === SessionState.ACTIVE) {
|
|
|
|
this._sessionState = SessionState.STOPPED;
|
|
|
|
this._sessionProxy.StopSync();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
_bailOutOnError(error) {
|
|
|
|
this._teardownPipeline();
|
2020-04-23 18:46:44 +00:00
|
|
|
this._unwatchSender();
|
2022-01-28 22:50:35 +00:00
|
|
|
this._stopSession();
|
|
|
|
|
|
|
|
log(`Recorder error: ${error.message}`);
|
|
|
|
|
|
|
|
if (this._onErrorCallback) {
|
2022-02-03 10:10:36 +00:00
|
|
|
this._onErrorCallback();
|
2022-01-28 22:50:35 +00:00
|
|
|
delete this._onErrorCallback;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this._startRequest) {
|
|
|
|
this._startRequest.reject(error);
|
|
|
|
delete this._startRequest;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this._stopRequest) {
|
|
|
|
this._stopRequest.reject(error);
|
|
|
|
delete this._stopRequest;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
_handleFatalPipelineError(message) {
|
|
|
|
this._pipelineState = PipelineState.ERROR;
|
|
|
|
this._bailOutOnError(new Error(`Fatal pipeline error: ${message}`));
|
|
|
|
}
|
|
|
|
|
|
|
|
_senderVanished() {
|
|
|
|
this._bailOutOnError(new Error('Sender has vanished'));
|
2020-04-23 18:46:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
_onSessionClosed() {
|
2022-01-28 22:50:35 +00:00
|
|
|
if (this._sessionState === SessionState.STOPPED)
|
|
|
|
return; // We closed the session ourselves
|
|
|
|
|
|
|
|
this._sessionState = SessionState.STOPPED;
|
|
|
|
this._bailOutOnError(new Error('Session closed unexpectedly'));
|
2020-04-23 18:46:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
_initSession(sessionPath) {
|
|
|
|
this._sessionProxy = new ScreenCastSessionProxy(Gio.DBus.session,
|
|
|
|
'org.gnome.Mutter.ScreenCast',
|
|
|
|
sessionPath);
|
|
|
|
this._sessionProxy.connectSignal('Closed', this._onSessionClosed.bind(this));
|
|
|
|
}
|
|
|
|
|
|
|
|
_startPipeline(nodeId) {
|
2021-06-09 14:42:52 +00:00
|
|
|
if (!this._ensurePipeline(nodeId))
|
|
|
|
return;
|
2020-04-23 18:46:44 +00:00
|
|
|
|
|
|
|
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;
|
|
|
|
|
2022-02-03 10:10:36 +00:00
|
|
|
this._startRequest.resolve();
|
|
|
|
delete this._startRequest;
|
2020-04-23 18:46:44 +00:00
|
|
|
}
|
|
|
|
|
2022-02-03 10:10:36 +00:00
|
|
|
startRecording() {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
this._startRequest = {resolve, reject};
|
|
|
|
|
|
|
|
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;
|
|
|
|
});
|
2020-04-23 18:46:44 +00:00
|
|
|
}
|
|
|
|
|
2022-02-03 10:10:36 +00:00
|
|
|
stopRecording() {
|
|
|
|
if (this._startRequest)
|
|
|
|
return Promise.reject(new Error('Unable to stop recorder while still starting'));
|
|
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
this._stopRequest = {resolve, reject};
|
|
|
|
|
|
|
|
this._pipelineState = PipelineState.FLUSHING;
|
|
|
|
this._pipeline.send_event(Gst.Event.new_eos());
|
|
|
|
});
|
2020-04-23 18:46:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
_onBusMessage(bus, message, _) {
|
|
|
|
switch (message.type) {
|
|
|
|
case Gst.MessageType.EOS:
|
2022-01-28 22:50:35 +00:00
|
|
|
this._teardownPipeline();
|
2020-04-23 18:46:44 +00:00
|
|
|
|
|
|
|
switch (this._pipelineState) {
|
|
|
|
case PipelineState.FLUSHING:
|
2022-01-28 22:50:35 +00:00
|
|
|
this._addRecentItem();
|
2020-04-23 18:46:44 +00:00
|
|
|
|
2022-01-28 22:50:35 +00:00
|
|
|
this._unwatchSender();
|
2020-04-23 18:46:44 +00:00
|
|
|
this._stopSession();
|
2022-01-28 22:50:35 +00:00
|
|
|
|
|
|
|
this._stopRequest.resolve();
|
|
|
|
delete this._stopRequest;
|
2020-04-23 18:46:44 +00:00
|
|
|
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);
|
screencastService: Improve the gstreamer pipeline for recording
The current gstreamer pipeline performs quite bad on slower machines and
is dropping lots of frames, improve the pipeline by changing a few
things:
- Use threads for videoconvert and improve speed of videoconvert by
disabling some unneeded things
- Add a queue before the encoding step, this allows the encoder to work
at its own pace and will lead to a lot more stability
- Remove the fixed quantizer and only set a max quantizer, this helps
quite a bit with performance
- Change the deadline parameter of vp8enc to 1: This makes the encoder
go into real time mode, which will make it a lot faster
- Set cpu-used to 16, the maximum possible value.
- Set static-threshold to 1000, static-threshold is the motion detection
threshold, and while a value of 100 is recommended for screencasting in
the gstreamer documentation (see [1]), using 1000 appears to perform a
lot better and still outputs fairly good quality
- Set a larger buffer size than the default size, this seems to get a
bit more stability during high load scenarios
All in all, those changes make the pipeline drop no more frames when
recording at 30 FPS and 2K screen resolution. That was tested on a
fairly recent mobile core-i5 processor.
Also, because we now have two %T replacement strings for the number of
threads, we need to switch to replaceAll(). For that to work, we have to
put the %T matching expression into quotes.
[1] https://gstreamer.freedesktop.org/documentation/vpx/GstVPXEnc.html?gi-language=c#GstVPXEnc:static-threshold
Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1633>
2021-01-26 08:31:49 +00:00
|
|
|
return pipelineDescr.replaceAll('%T', numThreads);
|
2020-04-23 18:46:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
_ensurePipeline(nodeId) {
|
|
|
|
const framerate = this._framerate;
|
2022-08-21 14:00:40 +00:00
|
|
|
const needsCopy =
|
2022-10-05 22:20:16 +00:00
|
|
|
Gst.Registry.get().check_feature_version('pipewiresrc', 0, 3, 57) &&
|
|
|
|
!Gst.Registry.get().check_feature_version('videoconvert', 1, 20, 4);
|
2020-04-23 18:46:44 +00:00
|
|
|
|
|
|
|
let fullPipeline = `
|
|
|
|
pipewiresrc path=${nodeId}
|
2022-08-21 14:00:40 +00:00
|
|
|
always-copy=${needsCopy}
|
2020-04-23 18:46:44 +00:00
|
|
|
do-timestamp=true
|
|
|
|
keepalive-time=1000
|
|
|
|
resend-last=true !
|
|
|
|
video/x-raw,max-framerate=${framerate}/1 !
|
|
|
|
${this._pipelineString} !
|
2020-08-10 23:34:03 +00:00
|
|
|
filesink location="${this._filePath}"`;
|
2020-04-23 18:46:44 +00:00
|
|
|
fullPipeline = this._substituteThreadCount(fullPipeline);
|
|
|
|
|
2021-06-09 14:42:52 +00:00
|
|
|
try {
|
|
|
|
this._pipeline = Gst.parse_launch_full(fullPipeline,
|
|
|
|
null,
|
|
|
|
Gst.ParseFlags.FATAL_ERRORS);
|
2022-01-28 22:50:35 +00:00
|
|
|
} catch (e) {
|
|
|
|
this._handleFatalPipelineError(`Failed to create pipeline: ${e.message}`);
|
2021-06-09 14:42:52 +00:00
|
|
|
}
|
|
|
|
return !!this._pipeline;
|
2020-04-23 18:46:44 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
var ScreencastService = class extends ServiceImplementation {
|
2022-10-19 10:43:29 +00:00
|
|
|
static canScreencast() {
|
2022-10-24 12:01:03 +00:00
|
|
|
const elements = [
|
|
|
|
'pipewiresrc',
|
|
|
|
'filesink',
|
|
|
|
...DEFAULT_PIPELINE.split('!').map(e => e.trim().split(' ').at(0)),
|
|
|
|
];
|
2022-10-19 10:43:29 +00:00
|
|
|
return Gst.init_check(null) &&
|
2022-10-24 12:01:03 +00:00
|
|
|
elements.every(e => Gst.ElementFactory.find(e) != null);
|
2022-10-19 10:43:29 +00:00
|
|
|
}
|
|
|
|
|
2020-04-23 18:46:44 +00:00
|
|
|
constructor() {
|
|
|
|
super(ScreencastIface, '/org/gnome/Shell/Screencast');
|
|
|
|
|
2022-11-06 10:56:41 +00:00
|
|
|
this.hold(); // gstreamer initializing can take a bit
|
2022-10-19 10:43:29 +00:00
|
|
|
this._canScreencast = ScreencastService.canScreencast();
|
|
|
|
|
2020-04-23 18:46:44 +00:00
|
|
|
Gst.init(null);
|
2021-01-20 01:48:24 +00:00
|
|
|
Gtk.init();
|
2020-04-23 18:46:44 +00:00
|
|
|
|
2022-11-06 10:56:41 +00:00
|
|
|
this.release();
|
|
|
|
|
2020-04-23 18:46:44 +00:00
|
|
|
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');
|
|
|
|
}
|
|
|
|
|
2022-10-19 10:43:29 +00:00
|
|
|
get ScreencastSupported() {
|
|
|
|
return this._canScreencast;
|
|
|
|
}
|
|
|
|
|
2020-04-23 18:46:44 +00:00
|
|
|
_removeRecorder(sender) {
|
2022-01-28 22:50:35 +00:00
|
|
|
if (!this._recorders.delete(sender))
|
|
|
|
return;
|
|
|
|
|
2020-04-23 18:46:44 +00:00
|
|
|
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;
|
|
|
|
|
2023-01-04 13:03:14 +00:00
|
|
|
const videoDir =
|
|
|
|
GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_VIDEOS) ||
|
|
|
|
GLib.get_home_dir();
|
|
|
|
|
2020-04-23 18:46:44 +00:00
|
|
|
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();
|
2022-05-20 16:40:35 +00:00
|
|
|
const datestr = datetime.format('%Y-%m-%d');
|
2020-04-23 18:46:44 +00:00
|
|
|
|
2022-05-20 16:40:35 +00:00
|
|
|
filename += datestr;
|
2020-04-23 18:46:44 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
case 't': {
|
|
|
|
const datetime = GLib.DateTime.new_now_local();
|
2022-05-20 16:40:35 +00:00
|
|
|
const datestr = datetime.format('%H-%M-%S');
|
2020-04-23 18:46:44 +00:00
|
|
|
|
2022-05-20 16:40:35 +00:00
|
|
|
filename += datestr;
|
2020-04-23 18:46:44 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
default:
|
|
|
|
log(`Warning: Unknown escape ${c}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
escape = false;
|
|
|
|
} else if (c === '%') {
|
|
|
|
escape = true;
|
|
|
|
} else {
|
|
|
|
filename += c;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
if (escape)
|
|
|
|
filename += '%';
|
|
|
|
|
|
|
|
return this._getAbsolutePath(filename);
|
|
|
|
}
|
|
|
|
|
2022-02-03 10:10:36 +00:00
|
|
|
async ScreencastAsync(params, invocation) {
|
2020-04-23 18:46:44 +00:00
|
|
|
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,
|
2020-09-02 14:26:34 +00:00
|
|
|
filePath,
|
2020-04-23 18:46:44 +00:00
|
|
|
options,
|
|
|
|
invocation,
|
2022-02-03 10:10:36 +00:00
|
|
|
() => this._removeRecorder(sender));
|
2020-04-23 18:46:44 +00:00
|
|
|
} catch (error) {
|
|
|
|
log(`Failed to create recorder: ${error.message}`);
|
|
|
|
invocation.return_value(GLib.Variant.new('(bs)', returnValue));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this._addRecorder(sender, recorder);
|
|
|
|
|
|
|
|
try {
|
2022-02-03 10:10:36 +00:00
|
|
|
await recorder.startRecording();
|
|
|
|
returnValue = [true, filePath];
|
2020-04-23 18:46:44 +00:00
|
|
|
} catch (error) {
|
|
|
|
log(`Failed to start recorder: ${error.message}`);
|
|
|
|
this._removeRecorder(sender);
|
2022-02-03 10:10:36 +00:00
|
|
|
} finally {
|
2020-04-23 18:46:44 +00:00
|
|
|
invocation.return_value(GLib.Variant.new('(bs)', returnValue));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-03 10:10:36 +00:00
|
|
|
async ScreencastAreaAsync(params, invocation) {
|
2020-04-23 18:46:44 +00:00
|
|
|
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,
|
2022-02-03 10:10:36 +00:00
|
|
|
() => this._removeRecorder(sender));
|
2020-04-23 18:46:44 +00:00
|
|
|
} catch (error) {
|
|
|
|
log(`Failed to create recorder: ${error.message}`);
|
|
|
|
invocation.return_value(GLib.Variant.new('(bs)', returnValue));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this._addRecorder(sender, recorder);
|
|
|
|
|
|
|
|
try {
|
2022-02-03 10:10:36 +00:00
|
|
|
await recorder.startRecording();
|
|
|
|
returnValue = [true, filePath];
|
2020-04-23 18:46:44 +00:00
|
|
|
} catch (error) {
|
|
|
|
log(`Failed to start recorder: ${error.message}`);
|
|
|
|
this._removeRecorder(sender);
|
2022-02-03 10:10:36 +00:00
|
|
|
} finally {
|
2020-04-23 18:46:44 +00:00
|
|
|
invocation.return_value(GLib.Variant.new('(bs)', returnValue));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-03 10:10:36 +00:00
|
|
|
async StopScreencastAsync(params, invocation) {
|
2020-04-23 18:46:44 +00:00
|
|
|
const sender = invocation.get_sender();
|
|
|
|
|
|
|
|
const recorder = this._recorders.get(sender);
|
|
|
|
if (!recorder) {
|
|
|
|
invocation.return_value(GLib.Variant.new('(b)', [false]));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-02-03 10:10:36 +00:00
|
|
|
try {
|
|
|
|
await recorder.stopRecording();
|
|
|
|
} catch (error) {
|
|
|
|
log(`${sender}: Error while stopping recorder: ${error.message}`);
|
|
|
|
} finally {
|
2020-04-23 18:46:44 +00:00
|
|
|
this._removeRecorder(sender);
|
|
|
|
invocation.return_value(GLib.Variant.new('(b)', [true]));
|
2022-02-03 10:10:36 +00:00
|
|
|
}
|
2020-04-23 18:46:44 +00:00
|
|
|
}
|
|
|
|
};
|