diff --git a/data/pantheon.portal b/data/pantheon.portal index ad472038..b9de94e3 100644 --- a/data/pantheon.portal +++ b/data/pantheon.portal @@ -1,4 +1,4 @@ [portal] DBusName=org.freedesktop.impl.portal.desktop.pantheon -Interfaces=org.freedesktop.impl.portal.Access;org.freedesktop.impl.portal.AppChooser;org.freedesktop.impl.portal.Background;org.freedesktop.impl.portal.Screenshot;org.freedesktop.impl.portal.Wallpaper +Interfaces=org.freedesktop.impl.portal.Access;org.freedesktop.impl.portal.AppChooser;org.freedesktop.impl.portal.Background;org.freedesktop.impl.portal.Screenshot;org.freedesktop.impl.portal.Wallpaper;org.freedesktop.impl.portal.ScreenCast UseIn=pantheon diff --git a/src/Background/Portal.vala b/src/Background/Portal.vala index 58a1f55d..0e269bad 100644 --- a/src/Background/Portal.vala +++ b/src/Background/Portal.vala @@ -3,21 +3,6 @@ * SPDX-License-Identifier: LGPL-2.1-or-later */ -[DBus (name = "org.pantheon.gala.DesktopIntegration")] -private interface Gala.DesktopIntegration : Object { - public signal void running_applications_changed (); - - public const string NAME = "org.pantheon.gala"; - public const string PATH = "/org/pantheon/gala/DesktopInterface"; - - public struct RunningApplications { - string app_id; - HashTable details; - } - - public abstract async RunningApplications[] get_running_applications () throws DBusError, IOError; -} - [DBus (name = "org.freedesktop.impl.portal.Background")] public class Background.Portal : Object { public signal void running_applications_changed (); @@ -28,18 +13,13 @@ public class Background.Portal : Object { public Portal (DBusConnection connection) { this.connection = connection; - connection.get_proxy.begin ( - Gala.DesktopIntegration.NAME, - Gala.DesktopIntegration.PATH, - NONE, null, (obj, res) => { - try { - desktop_integration = connection.get_proxy.end (res); - desktop_integration.running_applications_changed.connect (() => running_applications_changed ()); - } catch { - warning ("Cannot connect to compositor, portal working with reduced functionality."); - } + Gala.DesktopIntegration.get_instance.begin ((obj, res) => { + desktop_integration = Gala.DesktopIntegration.get_instance.end (res); + + if (desktop_integration != null) { + desktop_integration.running_applications_changed.connect (() => running_applications_changed ()); } - ); + }); } [CCode (type_signature = "u")] diff --git a/src/DesktopIntegration.vala b/src/DesktopIntegration.vala new file mode 100644 index 00000000..5281840e --- /dev/null +++ b/src/DesktopIntegration.vala @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: 2023 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +[DBus (name = "org.pantheon.gala.DesktopIntegration")] +public interface Gala.DesktopIntegration : Object { + public struct RunningApplications { + string app_id; + HashTable details; + } + + public struct Window { + uint64 uid; + HashTable details; + } + + private const string NAME = "org.pantheon.gala"; + private const string PATH = "/org/pantheon/gala/DesktopInterface"; + + public signal void running_applications_changed (); + + public abstract async RunningApplications[] get_running_applications () throws DBusError, IOError; + public abstract async Window[] get_windows () throws DBusError, IOError; + + private static Gala.DesktopIntegration? instance; + + public static async Gala.DesktopIntegration? get_instance () { + if (instance != null) { + return instance; + } + + try { + instance = yield Bus.get_proxy (SESSION, NAME, PATH); + } catch (Error e) { + warning ("Cannot connect to compositor, portal working with reduced functionality."); + } + + return instance; + } +} diff --git a/src/ScreenCast/Dialog.vala b/src/ScreenCast/Dialog.vala new file mode 100644 index 00000000..6c633305 --- /dev/null +++ b/src/ScreenCast/Dialog.vala @@ -0,0 +1,184 @@ +/* + * SPDX-FileCopyrightText: 2024 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * Authored by: Leonhard Kargl + */ + +public class ScreenCast.Dialog : Granite.Dialog { + public SourceType source_types { get; construct; } + public bool allow_multiple { get; construct; } + + public int n_selected { get; private set; default = 0; } + + private List window_rows; + private List monitor_rows; + private SelectionRow? virtual_row; + + private Gtk.ListBox list_box; + private Gtk.CheckButton? group = null; + + public Dialog (SourceType source_types, bool allow_multiple) { + Object (source_types: source_types, allow_multiple: allow_multiple); + } + + construct { + window_rows = new List (); + monitor_rows = new List (); + + list_box = new Gtk.ListBox () { + vexpand = true + }; + list_box.add_css_class (Granite.STYLE_CLASS_RICH_LIST); + list_box.set_header_func (header_func); + + if (MONITOR in source_types) { + var monitor_tracker = new MonitorTracker (); + + foreach (var monitor in monitor_tracker.monitors) { + var row = new SelectionRow (MONITOR, monitor.connector, + monitor.display_name, null, allow_multiple ? null : group); + + monitor_rows.append (row); + setup_row (row); + } + } + + if (WINDOW in source_types) { + populate_windows.begin (); + } + + if (VIRTUAL in source_types) { + virtual_row = new SelectionRow (VIRTUAL, "unused", _("Entire Display"), + new ThemedIcon ("preferences-desktop-display"), allow_multiple ? null : group); + setup_row (virtual_row); + } + + var scrolled_window = new Gtk.ScrolledWindow () { + child = list_box, + hscrollbar_policy = NEVER + }; + + var frame = new Gtk.Frame (null) { + child = scrolled_window + }; + + get_content_area ().append (frame); + + default_height = 400; + default_width = 300; + + add_button (_("Cancel"), Gtk.ResponseType.CANCEL); + + var accept_button = add_button (_("Share"), Gtk.ResponseType.ACCEPT); + accept_button.add_css_class (Granite.STYLE_CLASS_SUGGESTED_ACTION); + bind_property ("n-selected", accept_button, "sensitive", SYNC_CREATE, (binding, from_val, ref to_val) => { + to_val.set_boolean (n_selected > 0); + return true; + }); + } + + private async void populate_windows () { + var desktop_integration = yield Gala.DesktopIntegration.get_instance (); + + if (desktop_integration == null) { + return; + } + + Gala.DesktopIntegration.Window[] windows; + try { + windows = yield desktop_integration.get_windows (); + } catch (Error e) { + warning ("Failed to get windows from desktop integration: %s", e.message); + return; + } + + foreach (var window in windows) { + var label = _("Unknown Window"); + + if ("title" in window.details) { + label = (string) window.details["title"]; + } + + Icon icon = new ThemedIcon ("application-x-executable"); + if ("app-id" in window.details) { + var app_info = new DesktopAppInfo ((string) window.details["app-id"]); + if (app_info != null && app_info.get_icon () != null) { + icon = app_info.get_icon (); + } + } + + var row = new SelectionRow (WINDOW, window.uid, + label, icon, allow_multiple ? null : group); + + window_rows.append (row); + setup_row (row); + } + } + + private void setup_row (SelectionRow row) { + group = row.check_button; + + list_box.append (row); + + row.notify["selected"].connect (() => { + if (row.selected) { + n_selected++; + } else { + n_selected--; + } + }); + } + + private void header_func (Gtk.ListBoxRow row, Gtk.ListBoxRow? prev) { + if (!(row is SelectionRow) && prev != null && !(prev is SelectionRow)) { + return; + } + + var selection_row = (SelectionRow) row; + + if (prev == null || ((SelectionRow) prev).source_type != selection_row.source_type) { + string label = ""; + + switch (selection_row.source_type) { + case WINDOW: + label = _("Windows"); + break; + + case MONITOR: + label = _("Monitors"); + break; + + case VIRTUAL: + label = _("Entire Display"); + break; + } + + selection_row.set_header (new Granite.HeaderLabel (label)); + } + } + + public uint64[] get_selected_windows () { + uint64[] result = {}; + foreach (var row in window_rows) { + if (row.selected) { + result += (uint64) row.id; + } + } + return result; + } + + public string[] get_selected_monitors () { + string[] result = {}; + foreach (var row in monitor_rows) { + if (row.selected) { + result += (string) row.id; + } + } + return result; + } + + public bool get_virtual () { + return virtual_row != null && virtual_row.selected; + } +} diff --git a/src/ScreenCast/MonitorTracker/Interface.vala b/src/ScreenCast/MonitorTracker/Interface.vala new file mode 100644 index 00000000..18122981 --- /dev/null +++ b/src/ScreenCast/MonitorTracker/Interface.vala @@ -0,0 +1,176 @@ +/*- + * Copyright (c) 2018 elementary LLC. + * + * This software is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this software; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + * + * Authored by: Corentin Noël + */ + +[DBus (name = "org.gnome.Mutter.DisplayConfig")] +public interface MutterDisplayConfigInterface : Object { + public abstract void get_resources (out uint serial, out MutterReadDisplayCrtc[] crtcs, out MutterReadDisplayOutput[] outputs, out MutterReadDisplayMode[] modes, out int max_screen_width, out int max_screen_height) throws Error; + public abstract void apply_configuration (uint serial, bool persistent, MutterWriteDisplayCrtc[] crtcs, MutterWriteDisplayOutput[] outputs) throws Error; + public abstract int change_backlight (uint serial, uint output, int value) throws Error; + public abstract void get_crtc_gamma (uint serial, uint crtc, out uint[] red, out uint[] green, out uint[] blue) throws Error; + public abstract void set_crtc_gamma (uint serial, uint crtc, uint[] red, uint[] green, uint[] blue) throws Error; + public abstract int power_save_mode { get; set; } + public signal void monitors_changed (); + public abstract void get_current_state (out uint serial, out MutterReadMonitor[] monitors, out MutterReadLogicalMonitor[] logical_monitors, out GLib.HashTable properties) throws Error; + public abstract void apply_monitors_config (uint serial, MutterApplyMethod method, MutterWriteLogicalMonitor[] logical_monitors, GLib.HashTable properties) throws Error; +} + +[CCode (type_signature = "u")] +public enum MutterApplyMethod { + VERIFY = 0, + TEMPORARY = 1, + PERSISTENT = 2 +} + +[CCode (type_signature = "u")] +public enum DisplayTransform { + NORMAL = 0, + ROTATION_90 = 1, + ROTATION_180 = 2, + ROTATION_270 = 3, + FLIPPED = 4, + FLIPPED_ROTATION_90 = 5, + FLIPPED_ROTATION_180 = 6, + FLIPPED_ROTATION_270 = 7; + + public string to_string () { + // These values are based on the direction that the physical display has been rotated from its original position. + // They should not reflect the rotation that must be applied to the contents on screen. + switch (this) { + case ROTATION_90: + return _("Clockwise"); + case ROTATION_180: + return _("Upside-down"); + case ROTATION_270: + return _("Counterclockwise"); + case FLIPPED: + return _("Flipped"); + case FLIPPED_ROTATION_90: + return _("Flipped Clockwise"); + case FLIPPED_ROTATION_180: + return _("Flipped Upside-down"); + case FLIPPED_ROTATION_270: + return _("Flipped Counterclockwise"); + default: + return _("None"); + } + } +} + +public struct MutterReadMonitorInfo { + public string connector; + public string vendor; + public string product; + public string serial; + public uint hash { + get { + return (connector + vendor + product + serial).hash (); + } + } +} + +public struct MutterReadMonitorMode { + public string id; + public int width; + public int height; + public double frequency; + public double preferred_scale; + public double[] supported_scales; + public GLib.HashTable properties; +} + +public struct MutterReadMonitor { + public MutterReadMonitorInfo monitor; + public MutterReadMonitorMode[] modes; + public GLib.HashTable properties; +} + +public struct MutterReadLogicalMonitor { + public int x; + public int y; + public double scale; + public DisplayTransform transform; + public bool primary; + public MutterReadMonitorInfo[] monitors; + public GLib.HashTable properties; +} + +public struct MutterWriteMonitor { + public string connector; + public string monitor_mode; + public GLib.HashTable properties; +} + +public struct MutterWriteLogicalMonitor { + public int x; + public int y; + public double scale; + public DisplayTransform transform; + public bool primary; + public MutterWriteMonitor[] monitors; +} + +public struct MutterReadDisplayCrtc { + public uint id; + public int64 winsys_id; + public int x; + public int y; + public int width; + public int height; + public int current_mode; + public DisplayTransform current_transform; + public DisplayTransform[] transforms; + public GLib.HashTable properties; +} + +public struct MutterWriteDisplayCrtc { + public uint id; + public int new_mode; + public int x; + public int y; + public DisplayTransform transform; + public uint[] outputs; + public GLib.HashTable properties; +} + +public struct MutterReadDisplayOutput { + public uint id; + public int64 winsys_id; + public int current_crtc; + public uint[] possible_crtcs; + public string connector_name; + public uint[] modes; + public uint[] clones; + public GLib.HashTable properties; +} + +public struct MutterWriteDisplayOutput { + public uint id; + public GLib.HashTable properties; +} + +public struct MutterReadDisplayMode { + public uint id; + public int64 winsys_id; + public uint width; + public uint height; + public double frequency; + public uint flags; +} diff --git a/src/ScreenCast/MonitorTracker/Monitor.vala b/src/ScreenCast/MonitorTracker/Monitor.vala new file mode 100644 index 00000000..cd7aeccb --- /dev/null +++ b/src/ScreenCast/MonitorTracker/Monitor.vala @@ -0,0 +1,35 @@ +/*- + * Copyright (c) 2018 elementary LLC. + * + * This software is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this software; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + * + * Authored by: Corentin Noël + */ + +public class ScreenCast.Monitor : GLib.Object { + public string connector { get; set; } + public string vendor { get; set; } + public string product { get; set; } + public string serial { get; set; } + public uint hash { + get { + return (connector + vendor + product + serial).hash (); + } + } + + public string display_name { get; set; } + public bool is_builtin { get; set; } +} diff --git a/src/ScreenCast/MonitorTracker/MonitorTracker.vala b/src/ScreenCast/MonitorTracker/MonitorTracker.vala new file mode 100644 index 00000000..4e588fdd --- /dev/null +++ b/src/ScreenCast/MonitorTracker/MonitorTracker.vala @@ -0,0 +1,88 @@ +/*- + * Copyright (c) 2018 elementary LLC. + * + * This software is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this software; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + * + * Authored by: Corentin Noël + */ + +public class ScreenCast.MonitorTracker : GLib.Object { + public Gee.LinkedList monitors { get; construct; } + + private MutterDisplayConfigInterface iface; + private uint current_serial; + + construct { + monitors = new Gee.LinkedList (); + try { + iface = Bus.get_proxy_sync (BusType.SESSION, "org.gnome.Mutter.DisplayConfig", "/org/gnome/Mutter/DisplayConfig"); + iface.monitors_changed.connect (get_monitor_config); + get_monitor_config (); + } catch (Error e) { + critical (e.message); + } + } + + public void get_monitor_config () { + MutterReadMonitor[] mutter_monitors; + MutterReadLogicalMonitor[] mutter_logical_monitors; + GLib.HashTable properties; + try { + iface.get_current_state (out current_serial, out mutter_monitors, out mutter_logical_monitors, out properties); + } catch (Error e) { + critical (e.message); + } + + foreach (var mutter_monitor in mutter_monitors) { + var monitor = get_monitor_by_hash (mutter_monitor.monitor.hash); + if (monitor == null) { + monitor = new ScreenCast.Monitor (); + monitors.add (monitor); + } + + monitor.connector = mutter_monitor.monitor.connector; + monitor.vendor = mutter_monitor.monitor.vendor; + monitor.product = mutter_monitor.monitor.product; + monitor.serial = mutter_monitor.monitor.serial; + var display_name_variant = mutter_monitor.properties.lookup ("display-name"); + if (display_name_variant != null) { + monitor.display_name = display_name_variant.get_string (); + } else { + monitor.display_name = monitor.connector; + } + + var is_builtin_variant = mutter_monitor.properties.lookup ("is-builtin"); + if (is_builtin_variant != null) { + monitor.is_builtin = is_builtin_variant.get_boolean (); + } else { + /* + * Absence of "is-builtin" means it's not according to the documentation. + */ + monitor.is_builtin = false; + } + } + } + + private ScreenCast.Monitor? get_monitor_by_hash (uint hash) { + foreach (var monitor in monitors) { + if (monitor.hash == hash) { + return monitor; + } + } + + return null; + } +} diff --git a/src/ScreenCast/Portal.vala b/src/ScreenCast/Portal.vala new file mode 100644 index 00000000..5177568d --- /dev/null +++ b/src/ScreenCast/Portal.vala @@ -0,0 +1,124 @@ +/* + * SPDX-FileCopyrightText: 2024 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * Authored by: Leonhard Kargl + */ + +[Flags] +public enum ScreenCast.SourceType { + MONITOR = 1, + WINDOW = 2, + VIRTUAL = 4, +} + +[Flags] +public enum ScreenCast.CursorMode { + HIDDEN = 1, + EMBEDDED = 2, + METADATA = 4, +} + +[DBus (name = "org.freedesktop.impl.portal.ScreenCast")] +public class ScreenCast.Portal : Object { + public SourceType available_source_types { get; default = MONITOR | WINDOW | VIRTUAL; } + public CursorMode available_cursor_modes { get; default = HIDDEN | EMBEDDED | METADATA; } + public uint version { get; default = 3; } + + private DBusConnection connection; + + private HashTable sessions; + + public Portal (DBusConnection connection) { + this.connection = connection; + sessions = new HashTable (str_hash, str_equal); + } + + public async void create_session ( + ObjectPath handle, + ObjectPath session_handle, + string app_id, + HashTable options, + out uint response, + out HashTable results + ) throws DBusError, IOError { + var session = new Session (); + try { + var session_register_id = connection.register_object (session_handle, session); + sessions[session_handle] = session; + session.closed.connect (() => { + connection.unregister_object (session_register_id); + sessions.remove (session_handle); + }); + } catch (Error e) { + warning ("Failed to export session object: %s", e.message); + throw new DBusError.OBJECT_PATH_IN_USE (e.message); + } + + if (!yield session.init ()) { // Todo: maybe allow cancelling via request object? In my test this didn't take longer than 100ms + throw new IOError.FAILED ("Failed to create mutter ScreenCast session."); + } + + response = 0; + results = new HashTable (str_hash, str_equal); + results["session_id"] = Uuid.string_random (); + } + + public async void select_sources ( + ObjectPath handle, + ObjectPath session_handle, + string app_id, + HashTable options, + out uint response, + out HashTable results + ) throws DBusError, IOError { + SourceType source_types = MONITOR; + if ("types" in options) { + source_types = (SourceType) options["types"]; + } + + bool multiple = false; + if ("multiple" in options) { + multiple = (bool) options["multiple"]; + } + + sessions[session_handle].select_sources (source_types, multiple); + + response = 0; + results = new HashTable (str_hash, str_equal); + } + + public async void start ( + ObjectPath handle, + ObjectPath session_handle, + string app_id, + string parent_window, + HashTable options, + out uint response, + out HashTable results + ) throws DBusError, IOError { + results = new HashTable (str_hash, str_equal); + + var session = sessions[session_handle]; + + uint _response = 2; + Session.PipeWireStream[] streams = {}; + session.started.connect ((session_response, session_streams) => { + _response = session_response; + streams = session_streams; + + start.callback (); + }); + + session.start (parent_window); + + yield; + + if (_response == 2) { + throw new IOError.FAILED ("Failed to get pipewire streams"); + } + + response = _response; + results["streams"] = streams; + } +} diff --git a/src/ScreenCast/SelectionRow.vala b/src/ScreenCast/SelectionRow.vala new file mode 100644 index 00000000..6e1dcc79 --- /dev/null +++ b/src/ScreenCast/SelectionRow.vala @@ -0,0 +1,46 @@ +/* + * SPDX-FileCopyrightText: 2024 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * Authored by: Leonhard Kargl + */ + +public class ScreenCast.SelectionRow : Gtk.ListBoxRow { + public SourceType source_type { get; construct; } + public Variant id { get; construct; } + public string label { get; construct; } + public Icon? icon { get; construct; } + public Gtk.CheckButton? group { get; construct; } + + public Gtk.CheckButton check_button { get; construct; } + + public bool selected { get; set; default = false; } + + public SelectionRow (SourceType source_type, Variant id, string label, Icon? icon, Gtk.CheckButton? group) { + Object ( + source_type: source_type, + id: id, + label: label, + icon: icon, + group: group + ); + } + + construct { + var box = new Gtk.Box (HORIZONTAL, 6); + + check_button = new Gtk.CheckButton (); + box.append (check_button); + check_button.set_group (group); + + if (icon != null) { + box.append (new Gtk.Image.from_gicon (icon)); + } + + box.append (new Gtk.Label (label) { ellipsize = MIDDLE }); + + child = box; + + check_button.bind_property ("active", this, "selected", DEFAULT); + } +} diff --git a/src/ScreenCast/Session.vala b/src/ScreenCast/Session.vala new file mode 100644 index 00000000..48d75843 --- /dev/null +++ b/src/ScreenCast/Session.vala @@ -0,0 +1,227 @@ +/* + * SPDX-FileCopyrightText: 2024 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * Authored by: Leonhard Kargl + */ + +[DBus (name = "org.gnome.Mutter.ScreenCast")] +private interface Mutter.ScreenCast : Object { + public abstract async ObjectPath create_session (HashTable options) throws DBusError, IOError; +} + +[DBus (name = "org.gnome.Mutter.ScreenCast.Session")] +private interface Mutter.ScreenCastSession : Object { + public signal void closed (); + + public abstract async ObjectPath record_area (int x, int y, int width, int height, HashTable properties) throws DBusError, IOError; + public abstract async ObjectPath record_monitor (string connector, HashTable properties) throws DBusError, IOError; + public abstract async ObjectPath record_virtual (HashTable properties) throws DBusError, IOError; + public abstract async ObjectPath record_window (HashTable properties) throws DBusError, IOError; + + public abstract async void start () throws DBusError, IOError; + public abstract async void stop () throws DBusError, IOError; +} + +[DBus (name = "org.gnome.Mutter.ScreenCast.Stream")] +private interface Mutter.ScreenCastStream : Object { + public abstract HashTable parameters { owned get; } + + public signal void pipe_wire_stream_added (uint node_id); +} + +[DBus (name = "org.freedesktop.impl.portal.Session")] +public class ScreenCast.Session : Object { + public struct PipeWireStream { + uint node_id; + HashTable properties; + } + + public signal void closed (HashTable details); + + internal signal void started (uint response, PipeWireStream[] streams); + + public uint version { get; default = 1; } + + private Mutter.ScreenCastSession session; + + private SourceType source_types; + private bool allow_multiple; + + private PipeWireStream[] streams = {}; + private int required_streams = 0; + + internal async bool init () { + Mutter.ScreenCast proxy; + try { + proxy = yield Bus.get_proxy (SESSION, "org.gnome.Mutter.ScreenCast", "/org/gnome/Mutter/ScreenCast"); + } catch (Error e) { + warning ("Failed to get proxy: %s", e.message); + return false; + } + + string session_handle; + try { + session_handle = yield proxy.create_session (new HashTable (str_hash, str_equal)); + } catch (Error e) { + warning ("Failed to create session: %s", e.message); + return false; + } + + try { + session = yield Bus.get_proxy (SESSION, "org.gnome.Mutter.ScreenCast", session_handle); + } catch (Error e) { + warning ("Failed to get session object: %s", e.message); + return false; + } + + return true; + } + + internal void select_sources (SourceType source_types, bool allow_multiple) { + this.source_types = source_types; + this.allow_multiple = allow_multiple; + } + + internal void start (string parent_window) { + var dialog = new Dialog (source_types, allow_multiple); + + try { + var parent = ExternalWindow.from_handle (parent_window); + parent.set_parent_of (dialog.get_surface ()); + } catch (Error e) { + warning ("Failed to set parent: %s", e.message); + } + + dialog.response.connect ((response) => { + dialog.destroy (); + + if (response == Gtk.ResponseType.CANCEL) { + started (1, streams); + } else { + setup_recording.begin (dialog); + } + }); + dialog.present (); + } + + private async void setup_recording (Dialog dialog) { + //Should we fail if one fails or if all fail? Currently it's all + foreach (var window in dialog.get_selected_windows ()) { + if (yield record_window (window)) { + required_streams++; + } + } + + foreach (var connector in dialog.get_selected_monitors ()) { + if (yield record_monitor (connector)) { + required_streams++; + } + } + + if (dialog.get_virtual () && yield record_virtual ()) { + required_streams++; + } + + if (required_streams == 0) { + warning ("At least one stream has to be successfully setup."); + started (2, streams); + return; + } + + try { + yield session.start (); + } catch (Error e) { + warning ("Failed to start mutter session: %s", e.message); + started (2, streams); + } + } + + private async bool record_window (uint64 uid) { + var options = new HashTable (str_hash, str_equal); + options["window-id"] = uid; + + ObjectPath path; + try { + path = yield session.record_window (options); + } catch (Error e) { + warning ("Failed to record window: %s", e.message); + return false; + } + + return yield setup_mutter_stream (path, WINDOW); + } + + private async bool record_monitor (string connector) { + ObjectPath path; + try { + path = yield session.record_monitor (connector, new HashTable (str_hash, str_equal)); + } catch (Error e) { + warning ("Failed to record virtual: %s", e.message); + return false; + } + + return yield setup_mutter_stream (path, MONITOR); + } + + private async bool record_virtual () { + ObjectPath path; + try { + path = yield session.record_virtual (new HashTable (str_hash, str_equal)); + } catch (Error e) { + warning ("Failed to record virtual: %s", e.message); + return false; + } + + return yield setup_mutter_stream (path, VIRTUAL); + } + + private async bool setup_mutter_stream (ObjectPath proxy_path, SourceType source_type) { + Mutter.ScreenCastStream mutter_stream; + try { + mutter_stream = yield Bus.get_proxy (SESSION, "org.gnome.Mutter.ScreenCast", proxy_path); + } catch (Error e) { + warning ("Failed to get mutter stream proxy: %s", e.message); + return false; + } + + mutter_stream.pipe_wire_stream_added.connect ((node_id) => { + var properties = new HashTable (str_hash, str_equal); + properties["source_type"] = source_type; + + if ("size" in mutter_stream.parameters) { + properties["size"] = mutter_stream.parameters["size"]; + } + + if ("position" in mutter_stream.parameters) { + properties["position"] = mutter_stream.parameters["position"]; + } + + PipeWireStream stream = { + node_id, + properties + }; + + pipe_wire_stream_added (stream); + }); + + return true; + } + + private void pipe_wire_stream_added (PipeWireStream pipe_wire_stream) { + streams += pipe_wire_stream; + if (streams.length == required_streams) { + started (0, streams); + } + } + + public async void close () throws DBusError, IOError { + try { + yield session.stop (); + } catch (Error e) { + warning ("Failed to close mutter ScreenCast session: %s", e.message); + } + + closed (new HashTable (str_hash, str_equal)); + } +} diff --git a/src/XdgDesktopPortalPantheon.vala b/src/XdgDesktopPortalPantheon.vala index f413ee79..56056120 100644 --- a/src/XdgDesktopPortalPantheon.vala +++ b/src/XdgDesktopPortalPantheon.vala @@ -46,6 +46,9 @@ private void on_bus_acquired (DBusConnection connection, string name) { connection.register_object ("/org/freedesktop/portal/desktop", new Wallpaper.Portal (connection)); debug ("Wallpaper Portal registered!"); + + connection.register_object ("/org/freedesktop/portal/desktop", new ScreenCast.Portal (connection)); + debug ("Screencast Portal registered!"); } catch (Error e) { critical ("Unable to register the object: %s", e.message); } diff --git a/src/meson.build b/src/meson.build index 90ffcddc..c17ab23e 100644 --- a/src/meson.build +++ b/src/meson.build @@ -8,10 +8,18 @@ executable( 'AppChooser/Portal.vala', 'Background/NotificationRequest.vala', 'Background/Portal.vala', + 'ScreenCast/MonitorTracker/Interface.vala', + 'ScreenCast/MonitorTracker/Monitor.vala', + 'ScreenCast/MonitorTracker/MonitorTracker.vala', + 'ScreenCast/Dialog.vala', + 'ScreenCast/Portal.vala', + 'ScreenCast/SelectionRow.vala', + 'ScreenCast/Session.vala', 'Screenshot/Portal.vala', 'Screenshot/SetupDialog.vala', 'Wallpaper/Portal.vala', configure_file(input: 'Config.vala.in', output: '@BASENAME@', configuration: conf_data), + 'DesktopIntegration.vala', 'ExternalWindow.vala', 'XdgDesktopPortalPantheon.vala', icon_res,