Add virtual display creation to a separate Python script
UI kicks off and tracks all virtual display processes, even across sessions
This commit is contained in:
parent
fd40c6ca84
commit
854d74d983
|
|
@ -307,7 +307,6 @@ export default class BreezyDesktopExtension extends Extension {
|
||||||
this._show_banner_binding = Globals.data_stream.bind_property('show-banner', this._overlay_content, 'show-banner', Gio.SettingsBindFlags.DEFAULT);
|
this._show_banner_binding = Globals.data_stream.bind_property('show-banner', this._overlay_content, 'show-banner', Gio.SettingsBindFlags.DEFAULT);
|
||||||
this._show_banner_connection = Globals.data_stream.connect('notify::show-banner', this._handle_show_banner_update.bind(this));
|
this._show_banner_connection = Globals.data_stream.connect('notify::show-banner', this._handle_show_banner_update.bind(this));
|
||||||
this._was_show_banner = Globals.data_stream.show_banner;
|
this._was_show_banner = Globals.data_stream.show_banner;
|
||||||
Globals.logger.log_debug(`BreezyDesktopExtension _effect_enable - show_banner: ${this._was_show_banner}`);
|
|
||||||
if (!this._was_show_banner) this._recenter_display();
|
if (!this._was_show_banner) this._recenter_display();
|
||||||
|
|
||||||
this._custom_banner_enabled_binding = Globals.data_stream.bind_property('custom-banner-enabled', this._overlay_content, 'custom-banner-enabled', Gio.SettingsBindFlags.DEFAULT);
|
this._custom_banner_enabled_binding = Globals.data_stream.bind_property('custom-banner-enabled', this._overlay_content, 'custom-banner-enabled', Gio.SettingsBindFlags.DEFAULT);
|
||||||
|
|
|
||||||
|
|
@ -468,7 +468,13 @@ export const VirtualMonitorEffect = GObject.registerClass({
|
||||||
// if we're the focused display, we'll double the timeline and wait for the first half to let other
|
// if we're the focused display, we'll double the timeline and wait for the first half to let other
|
||||||
// displays ease out first
|
// displays ease out first
|
||||||
this._distance_ease_focus = this._is_focused();
|
this._distance_ease_focus = this._is_focused();
|
||||||
const timeline_ms = this._distance_ease_focus ? 500 : 150;
|
const ease_out_timeline_ms = 150;
|
||||||
|
const pause_ms = 50;
|
||||||
|
const ease_in_timeline_ms = 500; // includes ease out and pause
|
||||||
|
const ease_in_begin_pct = (ease_out_timeline_ms + pause_ms) / ease_in_timeline_ms;
|
||||||
|
const timeline_ms = this._distance_ease_focus ?
|
||||||
|
ease_in_timeline_ms :
|
||||||
|
ease_out_timeline_ms;
|
||||||
|
|
||||||
this._distance_ease_start = this._current_display_distance;
|
this._distance_ease_start = this._current_display_distance;
|
||||||
this._distance_ease_timeline = Clutter.Timeline.new_for_actor(this.get_actor(), timeline_ms);
|
this._distance_ease_timeline = Clutter.Timeline.new_for_actor(this.get_actor(), timeline_ms);
|
||||||
|
|
@ -478,10 +484,10 @@ export const VirtualMonitorEffect = GObject.registerClass({
|
||||||
let progress = this._distance_ease_timeline.get_progress();
|
let progress = this._distance_ease_timeline.get_progress();
|
||||||
if (this._distance_ease_focus) {
|
if (this._distance_ease_focus) {
|
||||||
// if we're the focused display, wait for the first half of the timeline to pass
|
// if we're the focused display, wait for the first half of the timeline to pass
|
||||||
if (progress < 0.5) return;
|
if (progress < ease_in_begin_pct) return;
|
||||||
|
|
||||||
// treat the second half of the timeline as its own full progression
|
// treat the second half of the timeline as its own full progression
|
||||||
progress = (progress - 0.5) * 2;
|
progress = (progress - ease_in_begin_pct) / (1 - ease_in_begin_pct);
|
||||||
|
|
||||||
// put this display in front as it starts to easy in
|
// put this display in front as it starts to easy in
|
||||||
this.is_closest = true;
|
this.is_closest = true;
|
||||||
|
|
@ -489,8 +495,8 @@ export const VirtualMonitorEffect = GObject.registerClass({
|
||||||
this.is_closest = false;
|
this.is_closest = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._current_display_distance = this._distance_ease_start +
|
this._current_display_distance = this._distance_ease_start +
|
||||||
progress * (this._distance_ease_target - this._distance_ease_start);
|
(1 - Math.cos(progress * Math.PI)) / 2 * (this._distance_ease_target - this._distance_ease_start);
|
||||||
this._update_display_position_uniforms();
|
this._update_display_position_uniforms();
|
||||||
}).bind(this));
|
}).bind(this));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ cp $UI_BUILD_PATH/src/breezydesktop.gresource $PACKAGE_BREEZY_DIR
|
||||||
cp -r po/mo/* $PACKAGE_LOCALE_DIR
|
cp -r po/mo/* $PACKAGE_LOCALE_DIR
|
||||||
cp data/com.xronlinux.BreezyDesktop.gschema.xml $PACKAGE_SCHEMAS_DIR
|
cp data/com.xronlinux.BreezyDesktop.gschema.xml $PACKAGE_SCHEMAS_DIR
|
||||||
cp $UI_BUILD_PATH/src/breezydesktop $PACKAGE_BIN_DIR
|
cp $UI_BUILD_PATH/src/breezydesktop $PACKAGE_BIN_DIR
|
||||||
|
cp $UI_BUILD_PATH/src/virtualdisplay $PACKAGE_BIN_DIR
|
||||||
cp $UI_BUILD_PATH/data/com.xronlinux.BreezyDesktop.desktop $PACKAGE_APPS_DIR
|
cp $UI_BUILD_PATH/data/com.xronlinux.BreezyDesktop.desktop $PACKAGE_APPS_DIR
|
||||||
|
|
||||||
mkdir -p $PACKAGE_ICONS_DIR/64x64/apps
|
mkdir -p $PACKAGE_ICONS_DIR/64x64/apps
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ from .license import BREEZY_GNOME_FEATURES
|
||||||
from .settingsmanager import SettingsManager
|
from .settingsmanager import SettingsManager
|
||||||
from .shortcutdialog import bind_shortcut_settings
|
from .shortcutdialog import bind_shortcut_settings
|
||||||
from .statemanager import StateManager
|
from .statemanager import StateManager
|
||||||
from .virtualdisplay import VirtualMonitor
|
from .virtualdisplaymanager import VirtualDisplayManager
|
||||||
from .xrdriveripc import XRDriverIPC
|
from .xrdriveripc import XRDriverIPC
|
||||||
import gettext
|
import gettext
|
||||||
import logging
|
import logging
|
||||||
|
|
@ -60,7 +60,6 @@ class ConnectedDevice(Gtk.Box):
|
||||||
viewport_offset_y_scale = Gtk.Template.Child()
|
viewport_offset_y_scale = Gtk.Template.Child()
|
||||||
viewport_offset_y_adjustment = Gtk.Template.Child()
|
viewport_offset_y_adjustment = Gtk.Template.Child()
|
||||||
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(Gtk.Box, self).__init__()
|
super(Gtk.Box, self).__init__()
|
||||||
self.init_template()
|
self.init_template()
|
||||||
|
|
@ -85,6 +84,7 @@ class ConnectedDevice(Gtk.Box):
|
||||||
self.settings = SettingsManager.get_instance().settings
|
self.settings = SettingsManager.get_instance().settings
|
||||||
self.desktop_settings = SettingsManager.get_instance().desktop_settings
|
self.desktop_settings = SettingsManager.get_instance().desktop_settings
|
||||||
self.ipc = XRDriverIPC.get_instance()
|
self.ipc = XRDriverIPC.get_instance()
|
||||||
|
self.virtual_display_manager = VirtualDisplayManager.get_instance()
|
||||||
self.extensions_manager = ExtensionsManager.get_instance()
|
self.extensions_manager = ExtensionsManager.get_instance()
|
||||||
|
|
||||||
self.settings.bind('display-distance', self.display_distance_adjustment, 'value', Gio.SettingsBindFlags.DEFAULT)
|
self.settings.bind('display-distance', self.display_distance_adjustment, 'value', Gio.SettingsBindFlags.DEFAULT)
|
||||||
|
|
@ -139,6 +139,9 @@ class ConnectedDevice(Gtk.Box):
|
||||||
self._refresh_use_optimal_monitor_config(self.use_optimal_monitor_config_switch, None)
|
self._refresh_use_optimal_monitor_config(self.use_optimal_monitor_config_switch, None)
|
||||||
self.extensions_manager.connect('notify::breezy-enabled', self._handle_enabled_config)
|
self.extensions_manager.connect('notify::breezy-enabled', self._handle_enabled_config)
|
||||||
|
|
||||||
|
self.virtual_display_manager.connect('notify::displays', self._on_virtual_displays_update)
|
||||||
|
self._on_virtual_displays_update(self.virtual_display_manager, None)
|
||||||
|
|
||||||
self.connect("destroy", self._on_widget_destroy)
|
self.connect("destroy", self._on_widget_destroy)
|
||||||
|
|
||||||
def _handle_monitor_wrapping_scheme_setting_changed(self, settings, val):
|
def _handle_monitor_wrapping_scheme_setting_changed(self, settings, val):
|
||||||
|
|
@ -206,10 +209,11 @@ class ConnectedDevice(Gtk.Box):
|
||||||
reload_display_distance_toggle_button(widget)
|
reload_display_distance_toggle_button(widget)
|
||||||
|
|
||||||
def on_add_virtual_display(self, width, height):
|
def on_add_virtual_display(self, width, height):
|
||||||
VirtualMonitor(width, height, self.on_virtual_display_ready).create()
|
logger.info(f"Adding virtual display {width}x{height}")
|
||||||
|
self.virtual_display_manager.create_virtual_display(width, height, 60)
|
||||||
|
|
||||||
def on_virtual_display_ready(self):
|
def _on_virtual_displays_update(self, virtual_display_manager, val):
|
||||||
logger.info("Virtual display ready")
|
logger.info(f"Found {len(virtual_display_manager.displays)} virtual displays")
|
||||||
|
|
||||||
def _on_widget_destroy(self, widget):
|
def _on_widget_destroy(self, widget):
|
||||||
# self.state_manager.unbind_property('follow-mode', self.follow_mode_switch, 'active')
|
# self.state_manager.unbind_property('follow-mode', self.follow_mode_switch, 'active')
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,15 @@ configure_file(
|
||||||
install_mode: 'r-xr-xr-x'
|
install_mode: 'r-xr-xr-x'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
configure_file(
|
||||||
|
input: 'virtualdisplay.in',
|
||||||
|
output: 'virtualdisplay',
|
||||||
|
configuration: conf,
|
||||||
|
install: true,
|
||||||
|
install_dir: get_option('bindir'),
|
||||||
|
install_mode: 'r-xr-xr-x'
|
||||||
|
)
|
||||||
|
|
||||||
breezydesktop_sources = [
|
breezydesktop_sources = [
|
||||||
'../modules/PyXRLinuxDriverIPC/xrdriveripc.py',
|
'../modules/PyXRLinuxDriverIPC/xrdriveripc.py',
|
||||||
'__init__.py',
|
'__init__.py',
|
||||||
|
|
@ -48,6 +57,7 @@ breezydesktop_sources = [
|
||||||
'statemanager.py',
|
'statemanager.py',
|
||||||
'time.py',
|
'time.py',
|
||||||
'virtualdisplay.py',
|
'virtualdisplay.py',
|
||||||
|
'virtualdisplaymanager.py',
|
||||||
'verify.py',
|
'verify.py',
|
||||||
'window.py'
|
'window.py'
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,108 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# virtualdisplay.in
|
||||||
|
#
|
||||||
|
# Copyright 2024 Unknown
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program 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 General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
lib_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'lib')
|
||||||
|
sys.path.insert(0, lib_dir)
|
||||||
|
|
||||||
|
from logging.handlers import TimedRotatingFileHandler
|
||||||
|
|
||||||
|
config_home = os.environ.get('XDG_CONFIG_HOME', '~/.config')
|
||||||
|
config_dir = os.path.expanduser(config_home)
|
||||||
|
state_home = os.environ.get('XDG_STATE_HOME', '~/.local/state')
|
||||||
|
state_dir = os.path.expanduser(state_home)
|
||||||
|
breezy_state_dir = os.path.join(state_dir, 'breezy_gnome')
|
||||||
|
log_dir = os.path.join(breezy_state_dir, 'logs/ui')
|
||||||
|
os.makedirs(log_dir, exist_ok=True)
|
||||||
|
|
||||||
|
logger = logging.getLogger('breezy_ui')
|
||||||
|
logger.setLevel(logging.INFO)
|
||||||
|
logname = os.path.join(log_dir, "breezy_desktop.log")
|
||||||
|
handler = TimedRotatingFileHandler(logname, when="midnight", backupCount=30)
|
||||||
|
handler.suffix = "%Y%m%d"
|
||||||
|
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||||
|
handler.setFormatter(formatter)
|
||||||
|
logger.addHandler(handler)
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
import signal
|
||||||
|
import pydbus
|
||||||
|
import time
|
||||||
|
|
||||||
|
VERSION = '@VERSION@'
|
||||||
|
|
||||||
|
xdg_data_home = os.getenv('XDG_DATA_HOME', os.path.join(os.path.expanduser('~'), '.local', 'share'))
|
||||||
|
appdir = os.getenv('APPDIR', xdg_data_home)
|
||||||
|
locale_dir = os.path.join(appdir, 'locale')
|
||||||
|
pkgdatadir = os.path.join(appdir, 'breezydesktop')
|
||||||
|
sys.path.insert(1, pkgdatadir)
|
||||||
|
|
||||||
|
import gi
|
||||||
|
|
||||||
|
gi.require_version('GLib', '2.0')
|
||||||
|
|
||||||
|
from gi.repository import GLib
|
||||||
|
|
||||||
|
def graceful_shutdown(signum, frame):
|
||||||
|
global virtual_display_instance
|
||||||
|
global loop
|
||||||
|
|
||||||
|
if virtual_display_instance is not None:
|
||||||
|
virtual_display_instance.terminate()
|
||||||
|
|
||||||
|
def _on_display_closed():
|
||||||
|
global loop
|
||||||
|
loop.quit()
|
||||||
|
|
||||||
|
def create_display(width, height, framerate):
|
||||||
|
global virtual_display_instance
|
||||||
|
|
||||||
|
virtual_display_instance = VirtualDisplay(width, height, framerate, _on_display_closed)
|
||||||
|
virtual_display_instance.create()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
from breezydesktop import virtualdisplay
|
||||||
|
from breezydesktop.virtualdisplay import VirtualDisplay
|
||||||
|
|
||||||
|
global virtual_display_instance
|
||||||
|
global loop
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description="Virtual display arguments")
|
||||||
|
parser.add_argument("--height", type=int, required=True, help="Height of the display")
|
||||||
|
parser.add_argument("--width", type=int, required=True, help="Width of the display")
|
||||||
|
parser.add_argument("--framerate", type=int, default=60, help="Framerate of the display")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
signal.signal(signal.SIGTERM, graceful_shutdown)
|
||||||
|
signal.signal(signal.SIGINT, graceful_shutdown)
|
||||||
|
|
||||||
|
loop = GLib.MainLoop()
|
||||||
|
|
||||||
|
try:
|
||||||
|
GLib.idle_add(create_display, args.width, args.height, args.framerate)
|
||||||
|
loop.run()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in main loop: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
import signal
|
import signal
|
||||||
import pydbus
|
import pydbus
|
||||||
import gi
|
import gi
|
||||||
|
import time
|
||||||
|
|
||||||
gi.require_version('Gst', '1.0')
|
gi.require_version('Gst', '1.0')
|
||||||
from gi.repository import GLib, GObject, Gst
|
from gi.repository import GLib, GObject, Gst
|
||||||
|
|
||||||
|
|
@ -13,32 +16,35 @@ logger = logging.getLogger('breezy_ui')
|
||||||
screen_cast_iface = 'org.gnome.Mutter.ScreenCast'
|
screen_cast_iface = 'org.gnome.Mutter.ScreenCast'
|
||||||
screen_cast_session_iface = 'org.gnome.Mutter.ScreenCast.Session'
|
screen_cast_session_iface = 'org.gnome.Mutter.ScreenCast.Session'
|
||||||
screen_cast_stream_iface = 'org.gnome.Mutter.ScreenCast.Session'
|
screen_cast_stream_iface = 'org.gnome.Mutter.ScreenCast.Session'
|
||||||
gst_pipeline_format = "pipewiresrc path=%u ! video/x-raw,max-framerate=60/1,width=%d,height=%d ! fakesink sync=false"
|
gst_pipeline_format = "pipewiresrc path=%u ! video/x-raw,max-framerate=%d/1,width=%d,height=%d ! fakesink sync=false"
|
||||||
|
|
||||||
|
class VirtualDisplay:
|
||||||
def _screen_cast_session():
|
def __init__(self, width, height, framerate, on_closed_cb):
|
||||||
bus = pydbus.SessionBus()
|
|
||||||
screen_cast = bus.get(screen_cast_iface, '/org/gnome/Mutter/ScreenCast')
|
|
||||||
session_path = screen_cast.CreateSession([])
|
|
||||||
logger.info("session path: %s" % session_path)
|
|
||||||
screen_cast_session = bus.get(screen_cast_iface, session_path)
|
|
||||||
|
|
||||||
return screen_cast_session
|
|
||||||
|
|
||||||
class VirtualMonitor:
|
|
||||||
def __init__(self, width, height, on_ready_cb):
|
|
||||||
self.width = width
|
self.width = width
|
||||||
self.height = height
|
self.height = height
|
||||||
self.on_ready_cb = on_ready_cb
|
self.framerate = framerate
|
||||||
|
self.on_closed_cb = on_closed_cb
|
||||||
|
|
||||||
Gst.init(None)
|
Gst.init(None)
|
||||||
|
|
||||||
|
def _screen_cast_session(self):
|
||||||
|
bus = pydbus.SessionBus()
|
||||||
|
screen_cast = bus.get(screen_cast_iface, '/org/gnome/Mutter/ScreenCast')
|
||||||
|
session_path = screen_cast.CreateSession([])
|
||||||
|
screen_cast_session = bus.get(screen_cast_iface, session_path)
|
||||||
|
|
||||||
|
return screen_cast_session
|
||||||
|
|
||||||
|
def _on_session_closed(self):
|
||||||
|
self.stream = None
|
||||||
|
self.terminate()
|
||||||
|
|
||||||
def create(self):
|
def create(self):
|
||||||
session = _screen_cast_session()
|
session = self._screen_cast_session()
|
||||||
|
session.onClosed = self._on_session_closed
|
||||||
stream_path = session.RecordVirtual({
|
stream_path = session.RecordVirtual({
|
||||||
'is-platform': GLib.Variant.new_boolean(True),
|
'is-platform': GLib.Variant.new_boolean(True),
|
||||||
})
|
})
|
||||||
logger.info("stream path: %s" % stream_path)
|
|
||||||
bus = pydbus.SessionBus()
|
bus = pydbus.SessionBus()
|
||||||
self.stream = bus.get(screen_cast_iface, stream_path)
|
self.stream = bus.get(screen_cast_iface, stream_path)
|
||||||
|
|
||||||
|
|
@ -47,25 +53,38 @@ class VirtualMonitor:
|
||||||
session.Start()
|
session.Start()
|
||||||
|
|
||||||
def terminate(self):
|
def terminate(self):
|
||||||
if self.stream is not None:
|
try:
|
||||||
self.stream.Stop()
|
if self.stream is not None:
|
||||||
|
self.stream.Stop()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to stop stream: %s" % e)
|
||||||
|
|
||||||
if self.pipeline is not None:
|
try:
|
||||||
self.pipeline.send_event(Gst.Event.new_eos())
|
if self.pipeline is not None:
|
||||||
self.pipeline.set_state(Gst.State.NULL)
|
self.pipeline.send_event(Gst.Event.new_eos())
|
||||||
|
self.pipeline.set_state(Gst.State.NULL)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to stop pipeline: %s" % e)
|
||||||
|
|
||||||
|
self.on_closed_cb()
|
||||||
|
|
||||||
def _on_message(self, bus, message):
|
def _on_message(self, bus, message):
|
||||||
type = message.type
|
type = message.type
|
||||||
logger.info("message type: %s" % type)
|
logger.info("message type: %s" % type)
|
||||||
if type == Gst.MessageType.EOS or type == Gst.MessageType.ERROR:
|
if type == Gst.MessageType.EOS:
|
||||||
|
self.pipeline = None
|
||||||
|
self.terminate()
|
||||||
|
elif type == Gst.MessageType.ERROR:
|
||||||
|
err, debug = message.parse_error()
|
||||||
|
logger.error("Error: %s" % err)
|
||||||
|
logger.error("Debug: %s" % debug)
|
||||||
self.terminate()
|
self.terminate()
|
||||||
|
|
||||||
def _on_pipewire_stream_added(self, node_id):
|
def _on_pipewire_stream_added(self, node_id):
|
||||||
logger.info("pipe wire stream added: %u" % node_id)
|
self.pipeline = Gst.parse_launch(gst_pipeline_format % (node_id, self.framerate, self.width, self.height))
|
||||||
|
|
||||||
self.pipeline = Gst.parse_launch(gst_pipeline_format % (node_id, self.width, self.height))
|
|
||||||
self.pipeline.set_state(Gst.State.PLAYING)
|
self.pipeline.set_state(Gst.State.PLAYING)
|
||||||
self.pipeline.get_bus().connect('message', self._on_message)
|
self.pipeline.get_bus().connect('message', self._on_message)
|
||||||
self.pipeline.set_state(Gst.State.PAUSED)
|
self.pipeline.set_state(Gst.State.PAUSED)
|
||||||
|
|
||||||
self.on_ready_cb()
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,122 @@
|
||||||
|
import gi
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import signal
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger('breezy_ui')
|
||||||
|
|
||||||
|
gi.require_version('GLib', '2.0')
|
||||||
|
from gi.repository import GLib, GObject
|
||||||
|
|
||||||
|
xdg_bin_home = os.getenv('XDG_BIN_HOME', os.path.join(os.path.expanduser('~'), '.local', 'bin'))
|
||||||
|
bindir = os.getenv('BINDIR', xdg_bin_home)
|
||||||
|
|
||||||
|
class VirtualDisplayManager(GObject.GObject):
|
||||||
|
__gproperties__ = {
|
||||||
|
'displays': (object, 'Displays', 'A list of the displays', GObject.ParamFlags.READWRITE)
|
||||||
|
}
|
||||||
|
_instance = None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_instance():
|
||||||
|
if not VirtualDisplayManager._instance:
|
||||||
|
VirtualDisplayManager._instance = VirtualDisplayManager()
|
||||||
|
|
||||||
|
return VirtualDisplayManager._instance
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
GObject.GObject.__init__(self)
|
||||||
|
|
||||||
|
self.shm_path = Path("/dev/shm/breezy_virtual_displays.json")
|
||||||
|
self._load_displays()
|
||||||
|
self._prune_dead_display_processes()
|
||||||
|
|
||||||
|
GLib.timeout_add_seconds(15, self._prune_dead_display_processes)
|
||||||
|
|
||||||
|
def _process_dead(self, pid):
|
||||||
|
if (not os.path.exists(f"/proc/{pid}")):
|
||||||
|
return True
|
||||||
|
|
||||||
|
try:
|
||||||
|
if (os.waitpid(pid, os.WNOHANG) == (pid, 0)):
|
||||||
|
return True
|
||||||
|
except ChildProcessError:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _prune_dead_display_processes(self):
|
||||||
|
self.set_property('displays', [disp for disp in self.displays if not self._process_dead(disp['pid'])])
|
||||||
|
self._save_processes()
|
||||||
|
|
||||||
|
return GLib.SOURCE_CONTINUE
|
||||||
|
|
||||||
|
def create_virtual_display(self, width, height, framerate) -> int:
|
||||||
|
self._create_virtual_display(width, height, framerate)
|
||||||
|
|
||||||
|
def _create_virtual_display(self, width, height, framerate):
|
||||||
|
try:
|
||||||
|
process = subprocess.Popen(
|
||||||
|
[f"{bindir}/virtualdisplay", "--width", str(width), "--height", str(height), "--framerate", str(framerate)],
|
||||||
|
start_new_session=True,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if process.returncode is not None:
|
||||||
|
logger.error(f"Failed to create virtual display: {process.stderr.read()}")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.displays.append({
|
||||||
|
'pid': process.pid,
|
||||||
|
'width': width,
|
||||||
|
'height': height
|
||||||
|
})
|
||||||
|
self.set_property('displays', self.displays)
|
||||||
|
self._save_processes()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to create virtual display: {e}")
|
||||||
|
|
||||||
|
def destroy_virtual_display(self, pid: str) -> bool:
|
||||||
|
try:
|
||||||
|
# Send SIGTERM to allow graceful shutdown
|
||||||
|
os.killpg(pid, signal.SIGTERM)
|
||||||
|
self.set_property('displays', [disp for disp in self.displays if disp['pid'] != pid])
|
||||||
|
self._save_processes()
|
||||||
|
return True
|
||||||
|
except ProcessLookupError:
|
||||||
|
# Process already gone, delete pid from list
|
||||||
|
self.set_property('displays', [disp for disp in self.displays if disp['pid'] != pid])
|
||||||
|
self._save_processes()
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to kill process {pid}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _save_processes(self):
|
||||||
|
with open(self.shm_path, 'w') as f:
|
||||||
|
json.dump(self.displays, f)
|
||||||
|
|
||||||
|
def _load_displays(self):
|
||||||
|
displays = []
|
||||||
|
if self.shm_path.exists():
|
||||||
|
try:
|
||||||
|
with open(self.shm_path, 'r') as f:
|
||||||
|
displays = json.load(f)
|
||||||
|
except Exception:
|
||||||
|
displays = []
|
||||||
|
|
||||||
|
self.set_property('displays', displays)
|
||||||
|
|
||||||
|
def do_set_property(self, prop, value):
|
||||||
|
if prop.name == 'displays':
|
||||||
|
self.displays = value
|
||||||
|
|
||||||
|
def do_get_property(self, prop):
|
||||||
|
if prop.name == 'displays':
|
||||||
|
return self.displays
|
||||||
|
|
@ -43,6 +43,13 @@ class BreezydesktopWindow(Gtk.ApplicationWindow):
|
||||||
|
|
||||||
def __init__(self, skip_verification, **kwargs):
|
def __init__(self, skip_verification, **kwargs):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
self.connected_device = ConnectedDevice()
|
||||||
|
self.failed_verification = FailedVerification()
|
||||||
|
self.no_device = NoDevice()
|
||||||
|
self.no_driver = NoDriver()
|
||||||
|
self.no_extension = NoExtension()
|
||||||
|
self.no_license = NoLicense()
|
||||||
|
|
||||||
self._skip_verification = skip_verification
|
self._skip_verification = skip_verification
|
||||||
|
|
||||||
|
|
@ -54,13 +61,6 @@ class BreezydesktopWindow(Gtk.ApplicationWindow):
|
||||||
self.state_manager.connect('notify::enabled-features-list', self._handle_state_update)
|
self.state_manager.connect('notify::enabled-features-list', self._handle_state_update)
|
||||||
self.settings.connect('changed::debug-no-device', self._handle_settings_update)
|
self.settings.connect('changed::debug-no-device', self._handle_settings_update)
|
||||||
|
|
||||||
self.connected_device = ConnectedDevice()
|
|
||||||
self.failed_verification = FailedVerification()
|
|
||||||
self.no_device = NoDevice()
|
|
||||||
self.no_driver = NoDriver()
|
|
||||||
self.no_extension = NoExtension()
|
|
||||||
self.no_license = NoLicense()
|
|
||||||
|
|
||||||
self.license_action_needed_button.connect('clicked', self._on_license_button_clicked)
|
self.license_action_needed_button.connect('clicked', self._on_license_button_clicked)
|
||||||
self.missing_breezy_features_button.connect('clicked', self._on_license_button_clicked)
|
self.missing_breezy_features_button.connect('clicked', self._on_license_button_clicked)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue