You are not logged in.

#1 Yesterday 13:43:02

Xwang
Member
From: EU
Registered: 2012-05-14
Posts: 418

Use v4l2loopback with signal and browsers

Hi to all,
I have a 2-in-1 laptop with an ipu6 webcam.
the camera is curreltly broken on kernel after 7.1.11 and on lts it works but it is upside down.
Moreover I'd like to be able to rotate the webacam to take in account the orientation of the pc (landscape, portrait, tent mode and so on) using the iio-sensor-proxy
So I've made with th help of AI a python code that for the moment just rotate it 180° to fix the lts.
It works when I call it with

 gst-launch-1.0 v4l2src device=/dev/video0 ! videoconvert ! autovideosink

but the camera is not visible in any application (qcam, signal-desktop, firefox, chromium).

In /usr/lib/modules-load.d/webcam-rotate.conf there is the following line:

v4l2loopback

and /usr/lib/modprobe.d/webcam-rotate.conf contains:

options v4l2loopback exclusive_caps=1 card_label="Corrected Webcam"

So the loopback is automatically loaded at startup as suggested in this https://bbs.archlinux.org/viewtopic.php?id=297557 and in the wiki.
Am I missing something?
Why it is not seen by the apps?

The /usr/lib/systemd/user/webcam-rotate.service executed the python script reported below:

# Copyright (C) 2026 andreak
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

[Unit]
Description=IPU6 webcam with corrected rotation (virtual PipeWire source)
After=pipewire.service
Requires=pipewire.service
StartLimitIntervalSec=60
StartLimitBurst=5
# If the IPU6 webcam is disabled from the BIOS, the PCI controller of
# the IPU6 bridge (0000:00:05.0 — fixed chipset address, independent
# of the sensor/webcam actually attached) either does not show up on
# the bus at all, or shows up without a bound driver. In both cases
# the "driver" symlink under its sysfs path does not exist, the
# condition fails, and the service stays "inactive (Condition check
# failed)" instead of attempting to start and failing.
ConditionPathExists=/sys/bus/pci/devices/0000:00:05.0/driver

[Service]
ExecStart=/usr/bin/webcam-rotate
Restart=on-failure
RestartSec=5
ExecStopPost=/bin/sh -c 'if [ "$SERVICE_RESULT" != "success" ]; then notify-send -u critical "Webcam" "Rotation-correction service failed to start: check journalctl --user -u webcam-rotate.service"; fi'

[Install]
WantedBy=default.target

This python script that applies the rotation is /usr/bin/webcam-rotate :

#!/usr/bin/env python3
# Copyright (C) 2026 andreak
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
webcam_rotate.py — Exposes the IPU6 webcam (via libcamera) as a new
PipeWire source with corrected rotation, independently of the app
consuming it (browser, Signal, etc.).

PHASE 1 (current): applies a fixed 180° rotation at startup, to
correct the Dell firmware defect (the SSDB sensor reports 0° instead
of 180°, and the "Latitude 9330" model is not in the kernel's DMI
quirk table — see ipu-bridge.c upside_down_sensor_dmi_ids[]).

PHASE 2 (future): the rotation will become dynamic, driven by
iio-sensor-proxy via D-Bus (net.hadess.SensorProxy interface,
AccelerometerOrientation property), to follow the device's physical
orientation in tablet mode during video calls.

Architecture designed for extension: rotation is not a static
parameter of the gst-launch pipeline, but a property of the
GStreamer "videoflip" element that can be set at runtime via
set_rotation(). Adding the sensor in Phase 2 just means wiring a
D-Bus callback to this same method — no pipeline rewrite needed.
"""

import sys
import glob
import time
import signal

import gi

gi.require_version("Gst", "1.0")
from gi.repository import Gst, GLib  # noqa: E402

# --- Configuration ------------------------------------------------------

# Label of the v4l2loopback device on which we publish the corrected
# feed. Must match "card_label" in the modprobe configuration of the
# v4l2loopback module (see /usr/lib/modprobe.d/webcam-rotate.conf).
# Unlike a synthetic PipeWire node, a real V4L2 device is recognized
# directly by browsers, Signal, and any other app, without going
# through the org.freedesktop.portal.Camera portal (which filters
# out anything but "real" webcams and ignores purely software
# PipeWire nodes).
V4L2LOOPBACK_LABEL = "Corrected Webcam"

# How long to wait, and how often to poll, for the v4l2loopback
# device to appear (small tolerance for possible race conditions at
# startup, e.g. module loaded by systemd-modules-load.service right
# before our unit).
DEVICE_WAIT_TIMEOUT_SEC = 5
DEVICE_WAIT_POLL_SEC = 0.5

# Map of "logical orientation" -> value accepted by videoflip's
# "method" property. In Phase 1 we only use ROTATE_180 (fixed
# correction); the full map is already in place for Phase 2.
FLIP_METHODS = {
    "normal": "none",
    "bottom-up": "rotate-180",
    "left-up": "clockwise",       # note: these two swap
    "right-up": "counterclockwise",  # width/height, see README below
}

# In Phase 1 the device is always physically mounted "upside down":
# the fixed correction we want to apply corresponds to "bottom-up".
DEFAULT_ORIENTATION = "bottom-up"


def find_v4l2loopback_device(label: str, timeout: float = DEVICE_WAIT_TIMEOUT_SEC) -> str:
    """
    Finds the /dev/videoN corresponding to the v4l2loopback device
    with the given label, by reading /sys/class/video4linux/videoN/name.

    We don't use a fixed number (video_nr) to avoid collisions if you
    plug in a real USB webcam in the future: the kernel assigns the
    next free number, and we find it by searching by name.
    """
    deadline = time.monotonic() + timeout
    while time.monotonic() < deadline:
        for name_path in glob.glob("/sys/class/video4linux/video*/name"):
            try:
                with open(name_path, encoding="utf-8") as f:
                    if f.read().strip() == label:
                        dev_num = name_path.split("/")[-2].removeprefix("video")
                        return f"/dev/video{dev_num}"
            except OSError:
                continue
        time.sleep(DEVICE_WAIT_POLL_SEC)

    raise RuntimeError(
        f"No v4l2loopback device found with label '{label}' "
        f"after {timeout}s. Is the v4l2loopback module loaded? "
        f"(lsmod | grep v4l2loopback)"
    )


class WebcamRotationService:
    """Manages the GStreamer pipeline and rotation at runtime."""

    def __init__(self, v4l2loopback_label: str = V4L2LOOPBACK_LABEL):
        self.v4l2loopback_label = v4l2loopback_label
        self.pipeline = None
        self.flip_element = None
        self.loop = GLib.MainLoop()

    def build_pipeline(self) -> None:
        """Builds and starts the libcamera -> videoflip -> v4l2sink pipeline."""
        device_path = find_v4l2loopback_device(self.v4l2loopback_label)
        print(f"[webcam_rotate] Found v4l2loopback device: {device_path}")

        pipeline_desc = (
            "libcamerasrc name=src ! "
            "videoconvert ! "
            "videoflip name=flip method=none ! "
            "videoconvert ! "
            f"v4l2sink name=sink device={device_path} sync=false"
        )

        self.pipeline = Gst.parse_launch(pipeline_desc)
        self.flip_element = self.pipeline.get_by_name("flip")

        bus = self.pipeline.get_bus()
        bus.add_signal_watch()
        bus.connect("message", self._on_bus_message)

        self.pipeline.set_state(Gst.State.PLAYING)

    def set_rotation(self, orientation: str) -> None:
        """
        Sets the rotation at runtime, without stopping the pipeline.

        `orientation` is one of the keys of FLIP_METHODS
        ("normal", "bottom-up", "left-up", "right-up").

        This is the hook point for Phase 2: the iio-sensor-proxy
        D-Bus callback will simply call this method every time the
        device's physical orientation changes.
        """
        method = FLIP_METHODS.get(orientation)
        if method is None:
            print(f"[webcam_rotate] Unknown orientation: {orientation}", file=sys.stderr)
            return

        if self.flip_element is None:
            print("[webcam_rotate] Pipeline not started yet", file=sys.stderr)
            return

        self.flip_element.set_property("method", method)
        print(f"[webcam_rotate] Rotation set: {orientation} -> {method}")

    def _on_bus_message(self, _bus, message) -> None:
        t = message.type
        if t == Gst.MessageType.ERROR:
            err, debug = message.parse_error()
            print(f"[webcam_rotate] GStreamer error: {err} ({debug})", file=sys.stderr)
            self.stop()
        elif t == Gst.MessageType.EOS:
            print("[webcam_rotate] Stream ended (EOS)")
            self.stop()

    def run(self) -> None:
        self.build_pipeline()
        # Phase 1: fixed rotation set right after startup.
        self.set_rotation(DEFAULT_ORIENTATION)

        # --- Phase 2 extension point ----------------------------------
        # The D-Bus watcher initialization for net.hadess.SensorProxy
        # goes here, calling self.set_rotation(...) on every
        # AccelerometerOrientation change.
        # Example (to be implemented in Phase 2):
        #
        #   from sensor_watcher import SensorWatcher
        #   self.watcher = SensorWatcher(on_change=self.set_rotation)
        #   self.watcher.start()
        # ---------------------------------------------------------------

        signal.signal(signal.SIGINT, lambda *_: self.stop())
        signal.signal(signal.SIGTERM, lambda *_: self.stop())

        try:
            self.loop.run()
        except KeyboardInterrupt:
            self.stop()

    def stop(self) -> None:
        if self.pipeline is not None:
            self.pipeline.set_state(Gst.State.NULL)
        if self.loop.is_running():
            self.loop.quit()


def main() -> int:
    Gst.init(None)
    service = WebcamRotationService()
    service.run()
    return 0


if __name__ == "__main__":
    sys.exit(main())

Offline

#2 Yesterday 14:31:07

seth
Member
From: Won't reply 2 private help req
Registered: 2012-09-03
Posts: 77,825

Re: Use v4l2loopback with signal and browsers

Just adding modprobe options won't auto-load the module, https://wiki.archlinux.org/title/Kernel_module#systemd

Offline

#3 Yesterday 14:59:42

Xwang
Member
From: EU
Registered: 2012-05-14
Posts: 418

Re: Use v4l2loopback with signal and browsers

seth wrote:

Just adding modprobe options won't auto-load the module, https://wiki.archlinux.org/title/Kernel_module#systemd

The /usr/lib/modules-load.d/webcam-rotate.conf contains the line:

v4l2loopback

and /usr/lib/modprobe.d/webcam-rotate.conf contains:

options v4l2loopback exclusive_caps=1 card_label="Corrected Webcam"

So the loopback is automatically loaded at startup with the options specified and indeed doing the lsmod I see it loaded ... isn't it the expected behavior with the file included in the /usr/lib/modules-load.d/ folder?

Offline

#4 Yesterday 15:24:45

seth
Member
From: Won't reply 2 private help req
Registered: 2012-09-03
Posts: 77,825

Re: Use v4l2loopback with signal and browsers

Yes, is. Is the module loaded after the boot?
Do you have access rights?

getfacl /dev/video*

Offline

#5 Yesterday 15:43:02

Xwang
Member
From: EU
Registered: 2012-05-14
Posts: 418

Re: Use v4l2loopback with signal and browsers

Yes, the module is loaded automatically, the python service works and rotate the camera, but the camera is visible only using  gst-launch-1.0 v4l2src device=/dev/video0 ! videoconvert ! autovideosink , not in browsers or signals. If I stop the python service, the built camera is accessible and works with browser and apps, but it is not the /dev/video0 device in that case and it is upside down in lts

lsmod | grep v4l2loopback
v4l2loopback           77824  1
videodev              421888  14 v4l2_async,v4l2_fwnode,videobuf2_v4l2,ov01a10,ov02c10,ivsc_csi,v4l2loopback,intel_ipu6_isys
[andreak@D9330 ~]$ getfacl /dev/video*
getfacl: Removing leading '/' from absolute path names
# file: dev/video0
# owner: root
# group: video
user::rw-
user:andreak:rw-
group::rw-
mask::rw-
other::---

My user is part of video group

Offline

#6 Yesterday 22:58:58

seth
Member
From: Won't reply 2 private help req
Registered: 2012-09-03
Posts: 77,825

Re: Use v4l2loopback with signal and browsers

If the disfunction is limited to specific clients (browsers) but works w/ eg. mpv/vlc/ffmpeg/gst*/… you're on spot for the exclusive_caps justification, https://github.com/v4l2loopback/v4l2loopback#options
However: https://github.com/v4l2loopback/v4l2loopback/issues/274

You can probably

systool -vm v4l2loopback

to confirm whether the options were correctly applied

Offline

Board footer

Powered by FluxBB