Selkies
Developer Referencewebcam

VirtualWebcam

Lazy owner of the process-wide pixelflux.VirtualCamera.

The camera is started on the first frame rather than at server start, so sessions that never enable the webcam pay nothing; a start failure is logged once and retried on a later frame.

Attributes

attributecameraOptional[Any]

The running camera, or None until the first successful start.

Functions

func__init__(self) -> None
Source Code
def __init__(self) -> None:
    self._cam: Optional[Any] = None
    self._lock = asyncio.Lock()
    self._start_failed_logged = False
    self._push_orientation: Optional[bool] = None
    self._device_mjpeg = False
    self._reformat_blocked_logged = False
    self._reformat_next_check = 0.0
paramself

Returns

None
funcneeds_ensure(self, codec) -> bool

Whether ensure has work to do for a frame of this codec.

The per-frame answer for a running camera whose format already suits the uplink is no, which is the whole of the hot path; the rest is the rare case where an auto device was shaped by an uplink of the other kind and may be worth re-creating.

Source Code
def needs_ensure(self, codec: Optional[int]) -> bool:
    """Whether ``ensure`` has work to do for a frame of this codec.

    The per-frame answer for a running camera whose format already suits the uplink is no,
    which is the whole of the hot path; the rest is the rare case where an ``auto`` device
    was shaped by an uplink of the other kind and may be worth re-creating.
    """
    if self._cam is None:
        return True
    # The kind comparison first: it answers every frame of a running camera, and the
    # setting lookup below builds a string the hot path has no use for.
    if codec is None or (codec == CODEC_MJPEG) == self._device_mjpeg:
        return False
    if not self._auto_format():
        return False
    return time.monotonic() >= self._reformat_next_check
paramself
paramcodecOptional[int]

Returns

bool
func_auto_format() -> bool
Source Code
@staticmethod
def _auto_format() -> bool:
    return str(app_settings.webcam_pixel_format or "auto").strip().lower() == "auto"

Returns

bool
func_consumers(self) -> Optional[str]

What is reading the camera right now, or None when nothing is.

Each sink answers for its own: the interposer counts the clients on its socket, PipeWire reports whether a consumer is linked to its node, and a kernel device's openers are found the only way a process can, through /proc. A pixelflux that cannot report the node's consumers is taken to have one: a re-created device would take the picture away from whoever is watching.

Source Code
def _consumers(self) -> Optional[str]:
    """What is reading the camera right now, or None when nothing is.

    Each sink answers for its own: the interposer counts the clients on its socket,
    PipeWire reports whether a consumer is linked to its node, and a kernel device's
    openers are found the only way a process can, through /proc. A pixelflux
    that cannot report the node's consumers is taken to have one: a re-created
    device would take the picture away from whoever is watching.
    """
    cam = self._cam
    if cam is None:
        return None
    try:
        stats = cam.stats()
    except Exception:
        return "unknown"
    if int(stats.get("clients", 0) or 0) > 0:
        return "interposer client"
    if "pipewire_streaming" in stats:
        if stats.get("pipewire_streaming"):
            return "PipeWire consumer"
    elif stats.get("pipewire"):
        return "a PipeWire node whose consumers this pixelflux does not report"
    device = str(stats.get("device_path") or "")
    if device and _device_has_openers(device):
        return f"an application holding {device}"
    return None
paramself

Returns

typing.Optional[str]
func_settings(self, codec) -> Any
Source Code
def _settings(self, codec: Optional[int]) -> Any:
    s = VirtualCameraSettings()
    s.socket_path = webcam_socket_path()
    s.width = int(app_settings.webcam_width)
    s.height = int(app_settings.webcam_height)
    s.fps_num = 30
    s.fps_den = 1
    s.pixel_format = device_pixel_format(str(app_settings.webcam_pixel_format), codec)
    s.device_path = str(app_settings.webcam_device)
    s.pipewire = bool(app_settings.webcam_pipewire[0])
    return s
paramself
paramcodecOptional[int]

Returns

typing.Any
funcensure(self, codec=None) -> Optional[Any]

Returns the running camera, starting it on first use (off the event loop).

An auto device already running in the other format is re-created for this codec when nothing is reading it, so a session that follows one of the other kind neither transcodes every frame (a video uplink fitted into an MJPEG device) nor decodes one it could have passed through. A device with a consumer is left exactly as it is: applications hold it open across mode switches, and pulling the format out from under one is worse than the transcode.

Source Code
async def ensure(self, codec: Optional[int] = None) -> Optional[Any]:
    """Returns the running camera, starting it on first use (off the event loop).

    An ``auto`` device already running in the other format is re-created for this codec
    when nothing is reading it, so a session that follows one of the other kind neither
    transcodes every frame (a video uplink fitted into an MJPEG device) nor decodes one it
    could have passed through. A device with a consumer is left exactly as it is:
    applications hold it open across mode switches, and pulling the format out from under
    one is worse than the transcode.

    Args:
        codec: Codec of the frame that brings the camera up, or that found the running
            device in the other format; an ``auto`` device format follows it (see
            ``device_pixel_format``).
    """
    if self._cam is not None:
        if not self.needs_ensure(codec):
            return self._cam
        async with self._lock:
            if self._cam is None or not self.needs_ensure(codec):
                return self._cam
            reader = await asyncio.to_thread(self._consumers)
            if reader is not None:
                self._reformat_next_check = time.monotonic() + REFORMAT_RECHECK_SECONDS
                if not self._reformat_blocked_logged:
                    self._reformat_blocked_logged = True
                    logger.info(
                        "Virtual webcam stays %s for %s: %s is reading it. Frames are "
                        "converted for the device; pin webcam_pixel_format to avoid it.",
                        "MJPEG" if self._device_mjpeg else "raw",
                        "an MJPEG uplink" if codec == CODEC_MJPEG else "a video uplink", reader)
                return self._cam
            logger.info("Virtual webcam re-created for the %s uplink now that nothing reads it.",
                        "MJPEG" if codec == CODEC_MJPEG else "video")
            await self._stop_locked()
    if not webcam_available():
        if not self._start_failed_logged:
            self._start_failed_logged = True
            logger.error("pixelflux VirtualCamera unavailable; webcam forwarding disabled.")
        return None
    async with self._lock:
        if self._cam is not None:
            return self._cam
        cam = VirtualCamera()
        try:
            settings = self._settings(codec)
            await asyncio.to_thread(cam.start, settings)
        except Exception as exc:
            if not self._start_failed_logged:
                self._start_failed_logged = True
                logger.error("Virtual webcam start failed: %s", exc)
            return None
        self._cam = cam
        self._device_mjpeg = str(settings.pixel_format).strip().upper() in ("MJPEG", "MJPG", "JPEG")
        self._reformat_blocked_logged = False
        self._reformat_next_check = 0.0
        stats = cam.stats()
        logger.info("Virtual webcam serving %s (%dx%d %s, kernel device: %s, PipeWire node: %s)",
                    cam.socket_path, settings.width, settings.height, settings.pixel_format,
                    cam.device_path or "none", "yes" if stats.get("pipewire") else "no")
        return cam
paramself
paramcodecOptional[int]
= None

Codec of the frame that brings the camera up, or that found the running device in the other format; an auto device format follows it (see device_pixel_format).

Returns

typing.Optional[typing.Any]
funcpush(self, data, codec, keyframe=False, offset=0, rotation=0, flip=False) -> int

Hands one encoded frame to the camera; returns its flags (KEYFRAME_WANTED bit).

The orientation is forwarded only when the frame carries one and the installed pixelflux takes it, so a build whose push predates the arguments keeps working (announced once; such frames are published as sent). A camera that is not running yet (ensure pending) drops the frame silently.

Source Code
def push(self, data: Any, codec: int, keyframe: bool = False, offset: int = 0,
         rotation: int = 0, flip: bool = False) -> int:
    """Hands one encoded frame to the camera; returns its flags (``KEYFRAME_WANTED`` bit).

    Args:
        rotation: Clockwise degrees (0/90/180/270) that make the decoded frame upright.
        flip: Horizontal mirror, applied after the rotation.

    The orientation is forwarded only when the frame carries one and the installed
    pixelflux takes it, so a build whose ``push`` predates the arguments keeps
    working (announced once; such frames are published as sent). A camera that is
    not running yet (``ensure`` pending) drops the frame silently.
    """
    cam = self._cam
    if cam is None:
        return 0
    try:
        if (rotation or flip) and self._orientation_accepted(cam):
            return int(cam.push(data, codec, keyframe, offset, rotation, flip))
        return int(cam.push(data, codec, keyframe, offset))
    except Exception as exc:
        logger.error("Virtual webcam push failed: %s", exc)
        return 0
paramself
paramdataAny
paramcodecint
paramkeyframebool
= False
paramoffsetint
= 0
paramrotationint
= 0

Clockwise degrees (0/90/180/270) that make the decoded frame upright.

paramflipbool
= False

Horizontal mirror, applied after the rotation.

Returns

int
func_orientation_accepted(self, cam) -> bool

Whether this camera's push takes the orientation arguments.

Decided once from the signature the extension publishes, rather than from a TypeError per frame: that exception is also what a bad argument raises, and mistaking one for an old build silently drops the orientation for the rest of the session. A build that publishes no signature is taken at its word, and a refusal then surfaces through push's own error path.

Source Code
def _orientation_accepted(self, cam: Any) -> bool:
    """Whether this camera's ``push`` takes the orientation arguments.

    Decided once from the signature the extension publishes, rather than from a
    ``TypeError`` per frame: that exception is also what a bad argument raises,
    and mistaking one for an old build silently drops the orientation for the
    rest of the session. A build that publishes no signature is taken at its
    word, and a refusal then surfaces through ``push``'s own error path.
    """
    if self._push_orientation is not None:
        return self._push_orientation
    try:
        params = inspect.signature(cam.push).parameters
        accepted = "rotation" in params or any(
            p.kind in (p.VAR_POSITIONAL, p.VAR_KEYWORD) for p in params.values())
    except (TypeError, ValueError):
        accepted = True
    self._push_orientation = accepted
    if not accepted:
        logger.warning(
            "Installed pixelflux takes no webcam orientation; rotated uplinks are published as sent.")
    return accepted
paramself
paramcamAny

Returns

bool
funckeyframe_wanted(self, flags) -> bool
Source Code
def keyframe_wanted(self, flags: int) -> bool:
    return bool(flags & getattr(VirtualCamera, "KEYFRAME_WANTED", 1))
paramself
paramflagsint

Returns

bool
func_stop_locked(self) -> None

Stops the running camera; the caller holds the lock.

Source Code
async def _stop_locked(self) -> None:
    """Stops the running camera; the caller holds the lock."""
    cam = self._cam
    self._cam = None
    if cam is not None:
        try:
            await asyncio.to_thread(cam.stop)
        except Exception:
            pass
paramself

Returns

None
funcstop(self) -> None
Source Code
async def stop(self) -> None:
    async with self._lock:
        await self._stop_locked()
paramself

Returns

None

On this page

Edit on GitHub