From 7e8feb047d8e1989ba1ae1fe5d8faa38f3f5fe60 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Tue, 8 Sep 2026 14:09:51 +0200 Subject: [PATCH] Relay the Elgato Cam Link 4K as a 16:9 virtual camera (#10809) * Relay the Elgato Cam Link 4K as a 16:9 virtual camera Browser meeting apps such as Zoom's web client ask the Cam Link for a standard-definition stream, and Chromium settles on the smallest mode it offers, 640x480. The Cam Link fills that 4:3 frame by cropping its 16:9 input, and the app then paints the frame into a 16:9 tile, so everyone comes out stretched wide. The web client has no HD switch to avoid it. Hide the raw capture node from users and re-expose it through v4l2-relayd as a 1280x720 virtual camera with the same name, so there is still just one "Cam Link 4K" to pick and no way to negotiate 4:3 from it. udev starts the relay whenever the Cam Link enumerates and stops it on unplug, and the relay only pulls frames while something is watching. The sink runs unsynced because v4l2src stamps each frame with its capture time, which a synced sink treats as already late and drops. Co-Authored-By: Claude Fable 5.1 * Take the review fixes for the Cam Link 4K relay Tie only the device's stop into the relay instance. A start dependency on it left a job waiting on a device that never comes whenever the base v4l2-relayd.service is started without a Cam Link attached, since the package generator wants every configured instance. Let the loopback unit rerun on each relay start, so a deleted or unloaded device is recreated on replug instead of the oneshot staying satisfied. Start the relay outright at the end of the migration. The udev trigger only starts it when the rule is new to the device, and a failed module build would otherwise pass silently with the raw camera already hidden. Run the hardware fix after the Panther Lake kernel swap, as it pulls in a DKMS module that would otherwise build twice. Co-Authored-By: Claude Fable 5.1 --------- Co-authored-by: Claude Fable 5.1 --- bin/omarchy-hw-elgato-camlink-4k | 5 +++++ .../system/camlink-4k-loopback.service | 9 +++++++++ .../camlink.conf | 16 +++++++++++++++ default/udev/elgato-camlink-4k.rules | 6 ++++++ default/v4l2-relayd/camlink.conf | 9 +++++++++ install/hardware/all.sh | 3 +++ install/hardware/fix-elgato-camlink-4k.sh | 18 +++++++++++++++++ migrations/1788862626.sh | 20 +++++++++++++++++++ 8 files changed, 86 insertions(+) create mode 100755 bin/omarchy-hw-elgato-camlink-4k create mode 100644 default/systemd/system/camlink-4k-loopback.service create mode 100644 default/systemd/system/v4l2-relayd@camlink.service.d/camlink.conf create mode 100644 default/udev/elgato-camlink-4k.rules create mode 100644 default/v4l2-relayd/camlink.conf create mode 100644 install/hardware/fix-elgato-camlink-4k.sh create mode 100644 migrations/1788862626.sh diff --git a/bin/omarchy-hw-elgato-camlink-4k b/bin/omarchy-hw-elgato-camlink-4k new file mode 100755 index 00000000..e94a54b2 --- /dev/null +++ b/bin/omarchy-hw-elgato-camlink-4k @@ -0,0 +1,5 @@ +#!/bin/bash + +# omarchy:summary=Detect whether an Elgato Cam Link 4K is plugged in. + +grep -qsx "Cam Link 4K" /sys/bus/usb/devices/*/product diff --git a/default/systemd/system/camlink-4k-loopback.service b/default/systemd/system/camlink-4k-loopback.service new file mode 100644 index 00000000..3b7d7b07 --- /dev/null +++ b/default/systemd/system/camlink-4k-loopback.service @@ -0,0 +1,9 @@ +[Unit] +Description=Create the Cam Link 4K virtual camera +After=systemd-modules-load.service + +[Service] +# Reruns on every relay start, so a deleted or unloaded device comes back. +Type=oneshot +ExecStart=/usr/bin/modprobe v4l2loopback +ExecStart=/bin/sh -c 'grep -qsx "Cam Link 4K" /sys/devices/virtual/video4linux/*/name || exec v4l2loopback-ctl add -n "Cam Link 4K" -x 1' diff --git a/default/systemd/system/v4l2-relayd@camlink.service.d/camlink.conf b/default/systemd/system/v4l2-relayd@camlink.service.d/camlink.conf new file mode 100644 index 00000000..956b2a41 --- /dev/null +++ b/default/systemd/system/v4l2-relayd@camlink.service.d/camlink.conf @@ -0,0 +1,16 @@ +[Unit] +# udev starts this when the Cam Link 4K appears. Only the device's stop is +# tied in: a start dependency on it would leave a job waiting whenever the +# base v4l2-relayd.service is started with no Cam Link attached. +ConditionPathExists=/dev/camlink4k +StopPropagatedFrom=dev-camlink4k.device +Requires=camlink-4k-loopback.service +After=camlink-4k-loopback.service + +[Service] +RestartSec=2 +# The packaged command line, plus sync=false on the sink. v4l2src stamps each +# frame with its capture time, so by the time it reaches the sink it is already +# past due and a synced sink drops every one of them. +ExecStart= +ExecStart=/bin/sh -c 'DEVICE=$(grep -l -m1 -E "^${CARD_LABEL}$" /sys/devices/virtual/video4linux/*/name | cut -d/ -f6); exec /usr/bin/v4l2-relayd -i "${VIDEOSRC}" $${SPLASHSRC:+-s "$${SPLASHSRC}"} -o "appsrc name=appsrc caps=video/x-raw,format=${FORMAT},width=${WIDTH},height=${HEIGHT},framerate=${FRAMERATE} ! videoconvert ! v4l2sink name=v4l2sink sync=false device=/dev/$${DEVICE}" $EXTRA_OPTS' diff --git a/default/udev/elgato-camlink-4k.rules b/default/udev/elgato-camlink-4k.rules new file mode 100644 index 00000000..27740681 --- /dev/null +++ b/default/udev/elgato-camlink-4k.rules @@ -0,0 +1,6 @@ +# Keep the Elgato Cam Link 4K's raw capture node away from users and hand it to +# the v4l2-relayd instance that re-exposes it as a 16:9 virtual camera. +# Sorted after 70-uaccess adds the tag and before 73-seat-late applies its ACL. +SUBSYSTEM=="video4linux", ENV{ID_VENDOR_ID}=="0fd9", ENV{ID_MODEL}=="Cam_Link_4K", ENV{ID_V4L_CAPABILITIES}==":capture:", \ + TAG-="uaccess", OWNER="root", GROUP="root", MODE="0600", SYMLINK+="camlink4k", \ + TAG+="systemd", ENV{SYSTEMD_ALIAS}="/dev/camlink4k", ENV{SYSTEMD_WANTS}="v4l2-relayd@camlink.service" diff --git a/default/v4l2-relayd/camlink.conf b/default/v4l2-relayd/camlink.conf new file mode 100644 index 00000000..7f4ce67e --- /dev/null +++ b/default/v4l2-relayd/camlink.conf @@ -0,0 +1,9 @@ +# Elgato Cam Link 4K relayed as a fixed 16:9 virtual camera. Browser meeting +# apps send 720p at most; raise WIDTH/HEIGHT (up to 3840x2160) for apps that +# can use more. +VIDEOSRC="v4l2src device=/dev/camlink4k" +FORMAT=NV12 +WIDTH=1280 +HEIGHT=720 +FRAMERATE=30/1 +CARD_LABEL="Cam Link 4K" diff --git a/install/hardware/all.sh b/install/hardware/all.sh index 7b806ce8..defeef1c 100644 --- a/install/hardware/all.sh +++ b/install/hardware/all.sh @@ -24,6 +24,9 @@ run_logged "$OMARCHY_INSTALL/hardware/intel/fred.sh" run_logged "$OMARCHY_INSTALL/hardware/intel/fix-wifi7-eht.sh" run_logged "$OMARCHY_INSTALL/hardware/intel/sof-firmware.sh" +# Pulls in v4l2loopback-dkms, so it sits after the Panther Lake kernel swap. +run_logged "$OMARCHY_INSTALL/hardware/fix-elgato-camlink-4k.sh" + # Rebuilds the boot image, so it has to follow the Panther Lake kernel swap # above rather than sit with the other Dell leaf at the top of this file. run_logged "$OMARCHY_INSTALL/hardware/dell-xps13-sidecar-amps.sh" diff --git a/install/hardware/fix-elgato-camlink-4k.sh b/install/hardware/fix-elgato-camlink-4k.sh new file mode 100644 index 00000000..f726fd9a --- /dev/null +++ b/install/hardware/fix-elgato-camlink-4k.sh @@ -0,0 +1,18 @@ +# Expose the Elgato Cam Link 4K as a 16:9-only virtual camera. +# Browsers ask it for 640x480, which it fills by cropping, and the 4:3 frame +# then gets stretched into a 16:9 tile. The raw node is hidden from users and +# v4l2-relayd re-exposes it at 1280x720 under the same name. + +if omarchy-hw-elgato-camlink-4k; then + mapfile -t kernel_headers < <(pacman -Qqs '^linux(-zen|-lts|-hardened|-t2|-ptl|-omarchy-bore)?$' | sed 's/$/-headers/') + omarchy-pkg-add "${kernel_headers[@]}" v4l2loopback-dkms v4l2loopback-utils v4l2-relayd + + sudo install -Dm644 "$OMARCHY_PATH/default/udev/elgato-camlink-4k.rules" /etc/udev/rules.d/71-elgato-camlink-4k.rules + sudo install -Dm644 "$OMARCHY_PATH/default/v4l2-relayd/camlink.conf" /etc/v4l2-relayd.d/camlink.conf + sudo install -Dm644 "$OMARCHY_PATH/default/systemd/system/camlink-4k-loopback.service" /etc/systemd/system/camlink-4k-loopback.service + sudo install -Dm644 "$OMARCHY_PATH/default/systemd/system/v4l2-relayd@camlink.service.d/camlink.conf" "/etc/systemd/system/v4l2-relayd@camlink.service.d/camlink.conf" + + # The module's own default device would otherwise show up in browsers as + # "Dummy video device" on machines without another relay. + echo "options v4l2loopback exclusive_caps=1" | sudo tee /etc/modprobe.d/v4l2loopback-exclusive-caps.conf >/dev/null +fi diff --git a/migrations/1788862626.sh b/migrations/1788862626.sh new file mode 100644 index 00000000..822d8513 --- /dev/null +++ b/migrations/1788862626.sh @@ -0,0 +1,20 @@ +echo "Expose the Elgato Cam Link 4K as a 16:9 virtual camera" + +if omarchy-hw-elgato-camlink-4k; then + source "$OMARCHY_PATH/install/hardware/fix-elgato-camlink-4k.sh" + + sudo systemctl daemon-reload + sudo udevadm control --reload + + # Re-run the rules for the Cam Link that is plugged in now so it gets hidden + # and relayed right away. The user ACL from its first plug survives a + # re-trigger, so drop it here; a replug or reboot does the same on its own. + sudo udevadm trigger --action=add --subsystem-match=video4linux + sudo udevadm settle + [[ -e /dev/camlink4k ]] && sudo setfacl -b /dev/camlink4k + + # The trigger only starts the relay when the rule is new to the device. + # Start it outright so a failed module build or loopback surfaces here, + # with the raw camera hidden, rather than passing silently. + sudo systemctl start v4l2-relayd@camlink.service +fi