Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ install-vm: install-both
$(INSTALL_DIR) $(DESTDIR)$(BINDIR)
$(INSTALL_PROGRAM) receiver/$(PKGNAME) $(DESTDIR)$(BINDIR)
$(INSTALL_DIR) $(DESTDIR)$(DATADIR)/$(PKGNAME)/receiver
$(INSTALL_PROGRAM) receiver/setup.sh receiver/receiver.py receiver/destroy.sh receiver/common.sh $(DESTDIR)$(DATADIR)/$(PKGNAME)/receiver
$(INSTALL_PROGRAM) receiver/setup.py receiver/receiver.py receiver/destroy.py $(DESTDIR)$(DATADIR)/$(PKGNAME)/receiver
$(INSTALL_DATA) receiver/qubes-video-companion.rules $(DESTDIR)/usr/lib/udev/rules.d/80-qubes-video-companion.rules
$(INSTALL_DATA) receiver/qubes-video-companion.modprobe $(DESTDIR)/usr/lib/modprobe.d/qubes-video-companion.conf
$(INSTALL_DATA) receiver/qubes-video-companion.sudoers $(DESTDIR)/etc/sudoers.d/qubes-video-companion
$(INSTALL_DIR) $(DESTDIR)$(SYSCONFDIR)/qubes/rpc-config
echo 'wait-for-session=1' > $(DESTDIR)$(SYSCONFDIR)/qubes/rpc-config/qvc.Webcam
echo 'wait-for-session=1' > $(DESTDIR)$(SYSCONFDIR)/qubes/rpc-config/qvc.ScreenShare
Expand Down
15 changes: 0 additions & 15 deletions receiver/common.sh

This file was deleted.

51 changes: 51 additions & 0 deletions receiver/destroy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/python3 --
#
# Copyright (C) 2025 Marek Marczykowski-Górecki
# <marmarek@invisiblethingslab.com>
# Licensed under the MIT License. See LICENSE file for details.

import errno
import sys
import os
import fcntl
import time
import subprocess


# V4L2LOOPBACK_CTL_REMOVE = 0x40487e02
# use legacy numbers since the change was recent
V4L2LOOPBACK_CTL_REMOVE = 0x4C81


def unregister_device(dev_nr):
ctrl_fd = os.open("/dev/v4l2loopback", os.O_RDWR)
try:
message = ("Please close any window that has an open video stream "
"so kernel modules can be securely unloaded...")

while True:
try:
fcntl.ioctl(ctrl_fd, V4L2LOOPBACK_CTL_REMOVE, dev_nr)
break
except OSError as e:
if e.errno == errno.EBUSY:
print(message, file=sys.stderr)
subprocess.call(
["notify-send", "Qubes Video Companion", message]
)
time.sleep(10)
else:
raise
finally:
os.close(ctrl_fd)


def main(argv):
if len(argv) != 2 or not argv[1].startswith("/dev/video"):
raise RuntimeError("Wrong arguments, expected /dev/video* path")
dev_nr = int(argv[1][len("/dev/video") :])
unregister_device(dev_nr)


if __name__ == "__main__":
main(sys.argv)
25 changes: 0 additions & 25 deletions receiver/destroy.sh

This file was deleted.

17 changes: 4 additions & 13 deletions receiver/qubes-video-companion
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,7 @@ esac
exit_clean () {
exit_code="$?"

/usr/share/qubes-video-companion/receiver/destroy.sh
sudo rm -f "$qvc_lock_file"
/usr/share/qubes-video-companion/receiver/destroy.py "$dev_path"

if [ "$video_source" = "webcam" ] && [ "$exit_code" = "141" ]; then
echo "The webcam device is in use! Please stop any instance of Qubes Video Companion running on another qube." >&2
Expand All @@ -67,15 +66,7 @@ exit_clean () {
exit "$exit_code"
}

qvc_lock_file="/run/lock/qubes-video-companion"
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. If you think this is an error, remove the lockfile $qvc_lock_file" >&2
exit 1
fi

/usr/share/qubes-video-companion/receiver/setup.sh
dev_path=$(/usr/share/qubes-video-companion/receiver/setup.py "$video_source")
trap exit_clean EXIT
# Filter standard error escape characters for safe printing to the terminal from the video sender
qrexec-client-vm --filter-escape-chars-stderr -- "$qube" "$qvc_service+$resolution" /usr/share/qubes-video-companion/receiver/receiver.py
qrexec-client-vm --filter-escape-chars-stderr -- "$qube" "$qvc_service+$resolution" /usr/share/qubes-video-companion/receiver/receiver.py "$dev_path"
1 change: 1 addition & 0 deletions receiver/qubes-video-companion.modprobe
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
options v4l2loopback devices=0
1 change: 1 addition & 0 deletions receiver/qubes-video-companion.rules
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
KERNEL=="v4l2loopback", GROUP="qubes", MODE="0660"
1 change: 1 addition & 0 deletions receiver/qubes-video-companion.sudoers
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
%qubes ALL = (root) NOPASSWD: /usr/sbin/modprobe v4l2loopback
11 changes: 8 additions & 3 deletions receiver/receiver.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,13 @@


def main(argv) -> NoReturn:
if len(argv) != 1:
raise RuntimeError("should not have any arguments")
dev_path = "/dev/video0"
if len(argv) == 2:
dev_path = argv[1]
elif len(argv) != 1:
raise RuntimeError(
"wrong arguments - expected only optional device path"
)

width, height, fps = read_video_parameters()

Expand Down Expand Up @@ -45,7 +50,7 @@ def main(argv) -> NoReturn:
"use-sink-caps=true",
"!",
"v4l2sink",
"device=/dev/video0",
"device=" + dev_path,
"sync=false",
),
)
Expand Down
113 changes: 113 additions & 0 deletions receiver/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
#!/usr/bin/python3 --
#
# Copyright (C) 2025 Marek Marczykowski-Górecki
# <marmarek@invisiblethingslab.com>
# Licensed under the MIT License. See LICENSE file for details.


import sys
import struct
import os
import fcntl
import subprocess

# see v4l2loopback.h:
# __s32 output_nr;
# __s32 unused; /*capture_nr;*/
#
# /**
# * a nice name for your device
# * if (*card_label)==0, an automatic name is assigned
# */
# char card_label[32];
#
# /**
# * allowed frame size
# * if too low, default values are used
# */
# __u32 min_width;
# __u32 max_width;
# __u32 min_height;
# __u32 max_height;
#
# /**
# * number of buffers to allocate for the queue
# * if set to <=0, default values are used
# */
# __s32 max_buffers;
#
# /**
# * how many consumers are allowed to open this device concurrently
# * if set to <=0, default values are used
# */
# __s32 max_openers;
#
# /**
# * set the debugging level for this device
# */
# __s32 debug;
#
# /**
# * whether to announce OUTPUT/CAPTURE capabilities exclusively
# * for this device or not
# * (!exclusive_caps)
# * NOTE: this is going to be removed once separate output/capture
# * devices are implemented
# */
# __s32 announce_all_caps;
v4l2_loopback_config_format = "ii32sIIIIiiii"


# V4L2LOOPBACK_CTL_ADD = 0x40487e01
# use legacy numbers since the change was recent
V4L2LOOPBACK_CTL_ADD = 0x4C80


def register_device(name):
ctrl_fd = os.open("/dev/v4l2loopback", os.O_RDWR)
try:
if not name:
name = "Qubes Video Companion"
# arg is v4l2_loopback_config config struct
conf = bytearray(
struct.pack(
v4l2_loopback_config_format,
-1, # output_nr (auto)
-1, # unused
name.encode(), # card_label
0, # min_width
0, # max_width
0, # min_height
0, # max_height
0, # max_buffers
0, # max_openers
0, # debug
0, # announce_all_caps
)
)
ret = fcntl.ioctl(ctrl_fd, V4L2LOOPBACK_CTL_ADD, conf)
return ret
finally:
os.close(ctrl_fd)


def main(argv):
name = None
if len(argv) == 2:
name = f"QVC - {argv[1]}"
elif len(argv) != 1:
raise RuntimeError("Invalid arguments - usage: setup.py [name]")
if not os.path.exists("/dev/v4l2loopback"):
subprocess.check_call(
["sudo", "--non-interactive", "modprobe", "v4l2loopback"]
)
# wait for udev to apply permission
subprocess.check_call(
["udevadm", "wait", "--settle", "/dev/v4l2loopback"]
)
dev_nr = register_device(name)
print(f"/dev/video{dev_nr}")


if __name__ == "__main__":
main(sys.argv)
25 changes: 0 additions & 25 deletions receiver/setup.sh

This file was deleted.

8 changes: 5 additions & 3 deletions rpm_spec/qubes-video-companion.spec.in
Original file line number Diff line number Diff line change
Expand Up @@ -103,12 +103,14 @@ This package contains the video-receiving portion of Qubes Video Companion.
%{_sysconfdir}/dkms/v4l2loopback.conf
%{_mandir}/man1/qubes-video-companion.1.gz
%{_bindir}/qubes-video-companion
%{_datadir}/qubes-video-companion/receiver/setup.sh
%{_datadir}/qubes-video-companion/receiver/setup.py
%{_datadir}/qubes-video-companion/receiver/receiver.py
%{_datadir}/qubes-video-companion/receiver/destroy.sh
%{_datadir}/qubes-video-companion/receiver/common.sh
%{_datadir}/qubes-video-companion/receiver/destroy.py
/usr/share/applications/qubes-video-companion-webcam.desktop
/usr/share/applications/qubes-video-companion-screenshare.desktop
/usr/lib/udev/rules.d/80-qubes-video-companion.rules
/usr/lib/modprobe.d/qubes-video-companion.conf
/etc/sudoers.d/qubes-video-companion

%package docs
Summary: Documentation for qubes-video-companion
Expand Down
10 changes: 10 additions & 0 deletions tests/qvctests/integ.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ def test_010_screenshare(self):
source_image = self.capture_from_screen(self.source)
destination_image = self.capture_from_video(self.view)
diff = self.compare_images(source_image, destination_image)
if diff >= 2.0:
with open(f"/tmp/window-dump-{self.id()}-source", "wb") as f:
f.write(source_image)
with open(f"/tmp/window-dump-{self.id()}-dest", "wb") as f:
f.write(destination_image)
self.assertLess(diff, 2.0)
self.click_stop(self.source, 'screenshare')
# wait for device to disappear, or a timeout
Expand Down Expand Up @@ -171,6 +176,11 @@ def test_020_webcam(self):
source_image = self.capture_from_video(self.source, ",width=640,height=480")
source_image = self.apply_mask(source_image)
diff = self.compare_images(source_image, destination_image)
if diff >= 2.5:
with open(f"/tmp/window-dump-{self.id()}-source", "wb") as f:
f.write(source_image)
with open(f"/tmp/window-dump-{self.id()}-dest", "wb") as f:
f.write(destination_image)
self.assertLess(diff, 2.5)


Expand Down