Back to all notes

Five Layers Down: Teaching a 7.0 Kernel to Fake a Webcam

A missing webcam is never one fix. Mine was five, and none of them were in the app.

I did not need a webcam. I needed a webcam that worked, and on this machine that turned out to be a five-layer problem.

The setup is ordinary: a Linux desktop, a phone, and Google Meet. DroidCam turns the phone into a system webcam, and there is an official Linux client for it. I installed the .deb, opened the client, and picked DroidCam Virtual Camera in Meet. Nothing happened. That is where most people stop, or give up, or buy a $30 webcam. This is what happened instead, because the failure was not in the app. It was five layers below it.

The whole project in one picture.

A webcam is a kernel module

A virtual webcam on Linux is not an app. It is a kernel module, v4l2loopback, that creates a fake video device real applications believe is a camera. Kernel modules have to be compiled against the exact headers of the running kernel, and that is where the trouble usually starts.

Layer one: the headers were never published

This machine runs a very new HWE kernel, 7.0.0-31-generic, whose headers existed only in Ubuntu's noble-proposed pocket. Proposed is disabled by default, for good reason. The first fix was not code. It was a decision: temporarily enable a bleeding-edge pocket, install the matching headers, then turn it back off. You cannot compile a driver against a kernel you cannot see.

Layer two: the packaged driver would not build

With headers in place, Ubuntu's v4l2loopback package (0.12.7) still refused to compile. Kernel 7.0 changed the signatures of v4l2_fh_add and v4l2_fh_del, both of which gained a struct file * argument, and the packaged module had not caught up.

This is where a lot of it does not work on Linux stories end. The fix is smaller than it sounds: find the two call sites, pass the new argument, rebuild. I patched the DKMS source and the module compiled. Compiling, however, is not the same as working.

Layer three: it compiled, then took the kernel down

The moment the client started its virtual camera, the kernel logged an oops:

RIP: vidioc_streamon - droidcam exited with irqs disabled

The patched 0.12.7 module was not merely incomplete. It was incompatible with kernel 7.0 in a way a compile fix could not paper over. A module that crashes while streaming video, with interrupts disabled, is telling you its internal assumptions about the kernel are wrong. The honest move was to stop patching and move to the source.

The crash, and the module that replaced it.

Layer four: replace the module with upstream

I removed 0.12.7 and built the current upstream v4l2loopback 0.15.4 via DKMS. Upstream understands kernel 7.0, and it provides the /dev/v4l2loopback control device the client expects. That is the difference between a driver that compiles and a driver that works: the version maintained for your kernel.

Layer five: the package was missing its own helper

Still nothing. The client's virtual-camera plugin shells out to v4l2loopback-ctl add -x 1 -n 'DroidCam Virtual Camera' to create the device, and that binary was absent from the .deb. The app was calling a tool nobody shipped.

I built v4l2loopback-ctl from the same upstream source, dropped it inside the client bundle, and the camera came up. One last ghost remained: the old broken module was pinned in memory and would not unload cleanly after the crashes. A reboot cleared it, and the new module now loads itself at boot. The setup has run daily since.

What is left after five fixes

None of the five layers were in the application. Every one was underneath it: kernel headers, a DKMS build, an ABI change, a kernel oops, and a missing binary inside a third-party package. Fixing it meant reading the errors literally, going one level down each time, and knowing when to stop patching and switch to the maintained source.

Two config files carry the daily setup, and they are the whole of it. The first loads the module at boot:

/etc/modules-load.d/v4l2loopback.conf
v4l2loopback

The second hands video devices to the video group, so the client can open them without root:

/etc/udev/rules.d/99-video.rules
SUBSYSTEM=="video4linux", KERNEL=="video*", GROUP="video", MODE="0660"

Neither file is clever. Together they are the difference between a camera that needs a shell every morning and one that is simply there.

The other tool in the loop

I fixed this the way I fix most things now: in conversation with an AI coding assistant. I described the symptom, it suggested where to look next, and I went and looked. A rubber duck that talks back.

What that bought me was speed of exploration, not answers. In a single afternoon I could test five or six theories for the same failure, most of them wrong, without paying the usual price for a wrong turn. Kernel headers in a pocket I had never enabled, an ABI change in 7.0, a runtime oops, the right upstream version, a helper binary missing from a package - any one of those could have eaten a day of documentation, forum threads, and dead ends. It took an afternoon.

The assistant did not know the answer. It knew the shape of where answers tend to live. The judgment was still mine: reading the oops literally, deciding that a patched 0.12.7 was a dead end and upstream 0.15.4 was not, noticing that nobody had shipped the tool the app was calling. AI shortened the search. It did not do the understanding.

That is how it went down. A problem that would once have meant days or weeks of reading and waiting on a mailing list became a few hours of directed probing - not because the machine is clever, but because it let me be wrong quickly and often. Being wrong quickly is most of what debugging is.

Giving it back

Separately, I sent the same instinct upstream. The DroidCam OBS plugin ships Linux users a zip they unpack by hand. I opened PR #33 adding a GitHub Actions workflow that repackages the official release into a native .deb and a .tar.xz, so Debian and Ubuntu users get apt install ./droidcam-obs_*.deb.

The interesting part was choosing not to build from source. Building in CI was fragile: the upstream Makefile's library names do not match Ubuntu's package names, and the default --as-needed linker flag silently drops libraries because the Makefile lists them before the sources. A build that looked successful still produced a plugin that failed to load with undefined symbol: lockdownd_client_new. Repackaging the official binary is the boring, reliable choice: the artifact is exactly what the maintainers tested, and if the input ever changes, the workflow fails loudly instead of quietly shipping something broken.

Validated on Ubuntu 24.04 with OBS 32.2.

Why I chase this kind of problem

This camera is not a product, and it will never appear on a résumé as a business result. It is the kind of problem I chase because the machine is right there and it is wrong, and I would rather understand why than route around it.

The layers change, kernel modules here and query plans or crawl pipelines somewhere else, but the method does not: read the error, go a level down, prefer the maintained source over the clever patch, and write it up so the next person does not have to rediscover it.