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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
David Heinemeier HanssonandClaude Fable 5.1 authored and GitHub committed 2026-09-08 14:09:51 +02:00
1 parent 148945000f
commit 7e8feb047d
8 files changed
+86

No files matched your search

+5
View File
@@ -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
@@ -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'
@@ -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'
+6
View File
@@ -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"
+9
View File
@@ -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"
+3
View File
@@ -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"
+18
View File
@@ -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
+20
View File
@@ -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