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