[gnome-shell] Move screencasting into a separate service process



commit 2b0731ab81887fd9767da73695c754c5b9aadd7c
Author: Jonas Ådahl <jadahl gmail com>
Date:   Thu Apr 23 20:46:44 2020 +0200

    Move screencasting into a separate service process
    
    Move the screencasting into a separate D-Bus service process, using
    PipeWire instead of Clutter API. The service is implemented in
    Javascript using the dbusService.js helper, and implements the same API
    as was done by screencast.js and the corresponding C code.
    
    https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1372

 .../org.gnome.Mutter.ScreenCast.xml                |  191 +++
 data/gnome-shell-dbus-interfaces.gresource.xml     |    1 +
 docs/reference/shell/meson.build                   |    5 -
 js/dbusServices/meson.build                        |    6 +
 .../org.gnome.Shell.Screencast.src.gresource.xml   |   11 +
 js/dbusServices/screencast/main.js                 |   11 +
 js/dbusServices/screencast/screencastService.js    |  458 ++++++
 js/js-resources.gresource.xml                      |    2 -
 js/misc/fileUtils.js                               |   43 +-
 js/ui/main.js                                      |   11 +-
 js/ui/panel.js                                     |    2 -
 js/ui/screencast.js                                |  146 --
 js/ui/status/screencast.js                         |   25 -
 meson.build                                        |    3 +-
 src/meson.build                                    |    9 -
 src/shell-recorder-src.c                           |  425 -----
 src/shell-recorder-src.h                           |   40 -
 src/shell-recorder.c                               | 1625 --------------------
 src/shell-recorder.h                               |   45 -
 19 files changed, 719 insertions(+), 2340 deletions(-)
---
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 0000000000..a8ff3ccb9b
--- /dev/null
+++ b/data/dbus-interfaces/org.gnome.Mutter.ScreenCast.xml
@@ -0,0 +1,191 @@
+<!DOCTYPE node PUBLIC
+'-//freedesktop//DTD D-BUS Object Introspection 1.0//EN'
+'http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd'>
+<node>
+
+  <!--
+      org.gnome.Mutter.ScreenCast:
+      @short_description: Screen cast interface
+
+      This API is private and not intended to be used outside of the integrated
+      system that uses libmutter. No compatibility between versions are
+      promised.
+  -->
+  <interface name="org.gnome.Mutter.ScreenCast">
+
+    <!--
+       CreateSession:
+       @properties: Properties
+       @session_path: Path to the new session object
+
+       * "remote-desktop-session-id" (s): The ID of a remote desktop session.
+                                          Remote desktop driven screen casts
+                                          are started and stopped by the remote
+                                          desktop session.
+       * "disable-animations" (b): Set to "true" if the screen cast application
+                                   would prefer animations to be globally
+                                   disabled, while the session is running. Default
+                                   is "false". Available since version 3.
+    -->
+    <method name="CreateSession">
+      <arg name="properties" type="a{sv}" direction="in" />
+      <arg name="session_path" type="o" direction="out" />
+    </method>
+
+    <!--
+        Version:
+        @short_description: API version
+    -->
+    <property name="Version" type="i" access="read" />
+
+  </interface>
+
+  <!--
+       org.gnome.Mutter.ScreenCast.Session:
+       @short_description: Screen cast session
+  -->
+  <interface name="org.gnome.Mutter.ScreenCast.Session">
+
+    <!--
+       Start:
+
+       Start the screen cast session
+    -->
+    <method name="Start" />
+
+    <!--
+       Stop:
+
+       Stop the screen cast session
+    -->
+    <method name="Stop" />
+
+    <!--
+       Closed:
+
+       The session has closed.
+    -->
+    <signal name="Closed" />
+
+    <!--
+       RecordMonitor:
+       @connector: Connector of the monitor to record
+       @properties: Properties
+       @stream_path: Path to the new stream object
+
+       Record a single monitor.
+
+       Available @properties include:
+
+       * "cursor-mode" (u): Cursor mode. Default: 'hidden' (see below)
+                            Available since API version 2.
+       * "is-recording" (b): Whether this is a screen recording. May be
+                             be used for choosing panel icon.
+                             Default: false. Available since API version 4.
+
+       Available cursor mode values:
+
+       0: hidden - cursor is not included in the stream
+       1: embedded - cursor is included in the framebuffer
+       2: metadata - cursor is included as metadata in the PipeWire stream
+    -->
+    <method name="RecordMonitor">
+      <arg name="connector" type="s" direction="in" />
+      <arg name="properties" type="a{sv}" direction="in" />
+      <arg name="stream_path" type="o" direction="out" />
+    </method>
+
+    <!--
+       RecordWindow:
+       @properties: Properties used determining what window to select
+       @stream_path: Path to the new stream object
+
+       Supported since API version 2.
+
+       Record a single window. The cursor will not be included.
+
+       Available @properties include:
+
+       * "window-id" (t): Id of the window to record.
+       * "cursor-mode" (u): Cursor mode. Default: 'hidden' (see RecordMonitor).
+       * "is-recording" (b): Whether this is a screen recording. May be
+                             be used for choosing panel icon.
+                             Default: false. Available since API version 4.
+
+    -->
+    <method name="RecordWindow">
+      <arg name="properties" type="a{sv}" direction="in" />
+      <arg name="stream_path" type="o" direction="out" />
+    </method>
+
+    <!--
+       RecordArea:
+       @x: X position of the recorded area
+       @y: Y position of the recorded area
+       @width: width of the recorded area
+       @height: height of the recorded area
+       @properties: Properties
+       @stream_path: Path to the new stream object
+
+       Record an area of the stage. The coordinates are in stage coordinates.
+       The size of the stream does not necessarily match the size of the
+       recorded area, and will depend on DPI scale of the affected monitors.
+
+       Available @properties include:
+
+       * "cursor-mode" (u): Cursor mode. Default: 'hidden' (see below)
+                            Available since API version 2.
+       * "is-recording" (b): Whether this is a screen recording. May be
+                             be used for choosing panel icon.
+                             Default: false. Available since API version 4.
+
+       Available cursor mode values:
+
+       0: hidden - cursor is not included in the stream
+       1: embedded - cursor is included in the framebuffer
+       2: metadata - cursor is included as metadata in the PipeWire stream
+    -->
+    <method name="RecordArea">
+      <arg name="x" type="i" direction="in" />
+      <arg name="y" type="i" direction="in" />
+      <arg name="width" type="i" direction="in" />
+      <arg name="height" type="i" direction="in" />
+      <arg name="properties" type="a{sv}" direction="in" />
+      <arg name="stream_path" type="o" direction="out" />
+    </method>
+  </interface>
+
+  <!--
+       org.gnome.Mutter.ScreenCast.Stream:
+       @short_description: Screen cast stream
+  -->
+  <interface name="org.gnome.Mutter.ScreenCast.Stream">
+
+    <!--
+       PipeWireStreamAdded:
+       @short_description: Pipewire stream added
+
+       A signal emitted when PipeWire stream for the screen cast stream has
+       been created. The @node_id corresponds to the PipeWire stream node.
+    -->
+    <signal name="PipeWireStreamAdded">
+      <annotation name="org.gtk.GDBus.C.Name" value="pipewire-stream-added"/>
+      <arg name="node_id" type="u" direction="out" />
+    </signal>
+
+    <!--
+       Parameters:
+       @short_description: Optional stream parameters
+
+       Available parameters include:
+
+       * "position" (ii): Position of the source of the stream in the
+                          compositor coordinate space.
+       * "size" (ii): Size of the source of the stream in the compositor
+                      coordinate space.
+    -->
+    <property name="Parameters" type="a{sv}" access="read" />
+
+  </interface>
+
+</node>
diff --git a/data/gnome-shell-dbus-interfaces.gresource.xml b/data/gnome-shell-dbus-interfaces.gresource.xml
index 8d335124af..69bc67458c 100644
--- a/data/gnome-shell-dbus-interfaces.gresource.xml
+++ b/data/gnome-shell-dbus-interfaces.gresource.xml
@@ -28,6 +28,7 @@
     <file preprocess="xml-stripblanks">org.freedesktop.UPower.xml</file>
     <file preprocess="xml-stripblanks">org.gnome.Magnifier.xml</file>
     <file preprocess="xml-stripblanks">org.gnome.Magnifier.ZoomRegion.xml</file>
+    <file preprocess="xml-stripblanks">org.gnome.Mutter.ScreenCast.xml</file>
     <file preprocess="xml-stripblanks">org.gnome.ScreenSaver.xml</file>
     <file preprocess="xml-stripblanks">org.gnome.SessionManager.EndSessionDialog.xml</file>
     <file preprocess="xml-stripblanks">org.gnome.SessionManager.Inhibitor.xml</file>
diff --git a/docs/reference/shell/meson.build b/docs/reference/shell/meson.build
index 9d8bbfd19b..5936ea3f69 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 c749f45dcb..2f047bb527 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 0000000000..e99b1ac181
--- /dev/null
+++ b/js/dbusServices/org.gnome.Shell.Screencast.src.gresource.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/org/gnome/Shell/Screencast/js">
+    <file>main.js</file>
+    <file>screencastService.js</file>
+    <file>dbusService.js</file>
+
+    <file>misc/config.js</file>
+    <file>misc/fileUtils.js</file>
+  </gresource>
+</gresources>
diff --git a/js/dbusServices/screencast/main.js b/js/dbusServices/screencast/main.js
new file mode 100644
index 0000000000..4a244264f0
--- /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 0000000000..99ad59f2eb
--- /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 2289a2ddee..07e134353c 100644
--- a/js/js-resources.gresource.xml
+++ b/js/js-resources.gresource.xml
@@ -92,7 +92,6 @@
     <file>ui/ripples.js</file>
     <file>ui/runDialog.js</file>
     <file>ui/screenShield.js</file>
-    <file>ui/screencast.js</file>
     <file>ui/screenshot.js</file>
     <file>ui/scripting.js</file>
     <file>ui/search.js</file>
@@ -137,7 +136,6 @@
     <file>ui/status/volume.js</file>
     <file>ui/status/bluetooth.js</file>
     <file>ui/status/remoteAccess.js</file>
-    <file>ui/status/screencast.js</file>
     <file>ui/status/system.js</file>
     <file>ui/status/thunderbolt.js</file>
   </gresource>
diff --git a/js/misc/fileUtils.js b/js/misc/fileUtils.js
index e672f68bd4..2f1798b84b 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 = `<interface name="${iface}">`;
+    let ifaceStopTag = '</interface>';
+    let ifaceStartIndex = xml.indexOf(ifaceStartTag);
+    let ifaceEndIndex = xml.indexOf(ifaceStopTag, ifaceStartIndex + 1) + ifaceStopTag.length;
+
+    let xmlHeader = '<!DOCTYPE node PUBLIC\n' +
+        '\'-//freedesktop//DTD D-BUS Object Introspection 1.0//EN\'\n' +
+        '\'http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd\'>\n' +
+        '<node>\n';
+    let xmlFooter = '</node>';
+
+    return (
+        xmlHeader +
+        xml.substr(ifaceStartIndex, ifaceEndIndex - ifaceStartIndex) +
+        xmlFooter);
+}
diff --git a/js/ui/main.js b/js/ui/main.js
index 99ea5201b8..bff730c5b2 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 90aea8dad3..4761903f38 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/meson.build b/meson.build
index 3c50502297..20ee6035a4 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 546d3a64b5..a7c56bbcfe 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
 )


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]