From 14ffef18aaa725c5c06423473c387ef3cdae2fde Mon Sep 17 00:00:00 2001 From: Ben Grande Date: Fri, 31 May 2024 14:31:00 +0200 Subject: [PATCH] Allow monitor choice If more than one monitor is found on the target, an interactive dialog will be prompted to the user to choose from, displaying monitor name (port) and resolution and geometry for better identification. The monitor can be enforced by the target by environment variable. Fixes: https://github.com/QubesOS/qubes-issues/issues/9275 --- doc/qubes-video-companion.rst | 4 ++++ qubes-rpc/services/qvc.ScreenShare | 26 +++++++++++++++++++++++++- qubes-rpc/services/qvc.Webcam | 8 +++++--- receiver/qubes-video-companion | 13 +++++++------ sender/screenshare.py | 22 ++++++++++++++++------ 5 files changed, 57 insertions(+), 16 deletions(-) diff --git a/doc/qubes-video-companion.rst b/doc/qubes-video-companion.rst index 3cdd7bc..c8a18ea 100644 --- a/doc/qubes-video-companion.rst +++ b/doc/qubes-video-companion.rst @@ -20,6 +20,10 @@ The project emphasizes correctness and security all the while also sporting supe OPTIONS ======= +resolution + The video resolution to stream and receive video in. The format is [WIDTHxHEIGHTxFPS], meaning resolution is optional. If you set the environment variable "QVC_MONITOR" in the target, that monitor is going to be preferred and if not found, will fallback to the primary monitor. Example: "1920x1080x60" + + video_source The video source to stream and receive video from. Either "webcam" or "screenshare". diff --git a/qubes-rpc/services/qvc.ScreenShare b/qubes-rpc/services/qvc.ScreenShare index 6878f05..a350ed3 100755 --- a/qubes-rpc/services/qvc.ScreenShare +++ b/qubes-rpc/services/qvc.ScreenShare @@ -1,7 +1,31 @@ #!/bin/sh -- - # Copyright (C) 2021 Elliot Killick # Copyright (C) 2021 Demi Marie Obenour # Licensed under the MIT License. See LICENSE file for details. + +set -eu + +## DISPLAY variable used by: xrandr, zenity export DISPLAY=:0 + +true "${XDG_RUNTIME_DIR:="/run/user/$(id -u)"}" +true "${DBUS_SESSION_BUS_ADDRESS="unix:path=${XDG_RUNTIME_DIR}/bus"}" +monitors="$(xrandr --listactivemonitors \ + | awk '/^ [0-9]+: \+/ { print "FALSE", $4, $3 }')" +monitor_count="$(echo "${monitors}" | wc -l)" +monitor_longest_line="$(echo "${monitors}" | wc -L)" +dialog_height="$((monitor_count*50+60))" +dialog_width="$((monitor_longest_line*10))" +if test "${monitor_count}" -gt 1; then + # shellcheck disable=SC2086 + QVC_MONITOR="$(zenity --list --radiolist \ + --height="${dialog_height}" --width="${dialog_width}" \ + --column "ID" --column "Name" --column "Resolution" \ + --title "Screen share" \ + --text "Select monitor to present to qube ${QREXEC_REMOTE_DOMAIN}" \ + ${monitors})" +fi +true "${QVC_MONITOR:=}" + +export XDG_RUNTIME_DIR DBUS_SESSION_BUS_ADDRESS QVC_MONITOR exec python3 -- /usr/share/qubes-video-companion/sender/screenshare.py diff --git a/qubes-rpc/services/qvc.Webcam b/qubes-rpc/services/qvc.Webcam index 7878428..8f66f6f 100755 --- a/qubes-rpc/services/qvc.Webcam +++ b/qubes-rpc/services/qvc.Webcam @@ -1,7 +1,9 @@ #!/bin/sh -- - # Copyright (C) 2021 Elliot Killick # Copyright (C) 2021 Demi Marie Obenour # Licensed under the MIT License. See LICENSE file for details. -export DISPLAY=:0 -exec python3 -- /usr/share/qubes-video-companion/sender/webcam.py ${1:+"$1"} +set -eu +true "${XDG_RUNTIME_DIR:="/run/user/$(id -u)"}" +true "${DBUS_SESSION_BUS_ADDRESS:="unix:path=${XDG_RUNTIME_DIR}/bus"}" +export DISPLAY=:0 XDG_RUNTIME_DIR DBUS_SESSION_BUS_ADDRESS +exec python3 -- /usr/share/qubes-video-companion/sender/webcam.py "${1:+"$1"}" diff --git a/receiver/qubes-video-companion b/receiver/qubes-video-companion index 6c7d310..6dcef46 100755 --- a/receiver/qubes-video-companion +++ b/receiver/qubes-video-companion @@ -12,7 +12,8 @@ unset GETOPT_COMPATIBLE name=${0##*/} usage() { - printf '%s: Usage: qubes-video-companion [--resolution=WIDTHxHEIGHTxFPS] [--] webcam|screenshare [destination qube]\n' "$name" + echo "Usage: $name [--resolution=[WIDTHxHEIGHTxFPS]] [--] webcam|screenshare [destination qube]" >&2 + echo "Resolution example: 1920x1080x60" exit "$1" } @@ -23,14 +24,14 @@ while :; do case $1 in -r|--resolution) if [[ -z "$2" ]]; then - printf '%s: Empty resolution argument\n' "$name" - usage 0 - fi >&2 + echo "$name: Empty resolution argument" >&2 + usage 1 + fi resolution=${2//@/+} resolution=${resolution//x/+} shift 2 ;; - --help) usage 0;; + -h|--help) usage 0;; --) shift; break;; *) exit 1;; # cannot happen esac @@ -71,7 +72,7 @@ if ! [ -f "$qvc_lock_file" ]; then trap exit_clean EXIT sudo touch "$qvc_lock_file" else - echo "Qubes Video Companion is already running! Please stop the previous session before starting a new one." >&2 + echo "Qubes Video Companion is already running! Please stop the previous session before starting a new one. If you think this is an error, remove the lockfile $qvc_lock_file" >&2 exit 1 fi diff --git a/sender/screenshare.py b/sender/screenshare.py index 020df95..8f29130 100644 --- a/sender/screenshare.py +++ b/sender/screenshare.py @@ -10,6 +10,7 @@ # pylint: disable=wrong-import-position import gi +import os gi.require_version("Gdk", "3.0") from gi.repository import Gdk @@ -30,15 +31,24 @@ def icon(self) -> str: return "video-display" def parameters(self) -> Tuple[int, int, int]: - monitor = Gdk.Display().get_default().get_monitor(0).get_geometry() + display = Gdk.Display().get_default() + monitor_count = display.get_n_monitors() + monitor_wanted = os.environ["QVC_MONITOR"] + ## If wanted monitor is not found, use the primary monitor (0). + monitor_index = 0 + for m in range(monitor_count): + if display.get_monitor(m).get_model() == monitor_wanted: + monitor_index = m + break + geometry = display.get_monitor(monitor_index).get_geometry() screen = Gdk.Screen().get_default() kwargs = { - "crop_t": monitor.y, - "crop_l": monitor.x, - "crop_r": screen.width() - monitor.x - monitor.width, - "crop_b": screen.height() - monitor.y - monitor.height, + "crop_t": geometry.y, + "crop_l": geometry.x, + "crop_r": screen.width() - geometry.x - geometry.width, + "crop_b": screen.height() - geometry.y - geometry.height, } - return (monitor.width, monitor.height, 30, kwargs) + return (geometry.width, geometry.height, 30, kwargs) def pipeline(self, width: int, height: int, fps: int, **kwargs) -> List[str]: