Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions .github/workflows/e2e-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
name: End-to-End GTK Tests

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

jobs:
build:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'

- name: Install System Dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
python3-pip \
xvfb \
python3-dogtail \
python3-gi \
python3-gi-cairo \
gir1.2-gtk-4.0 \
gir1.2-gstreamer-1.0 \
gstreamer1.0-plugins-base \
gstreamer1.0-plugins-good \
gstreamer1.0-plugins-bad \
v4l2loopback-utils \
ffmpeg \
xauth \
at-spi2-core

- name: Setup at-spi for Headless Testing
run: |
# Required to allow dogtail to run under xvfb
mkdir -p ~/.config/at-spi2
echo -e "[core]\nEnabled=true" > ~/.config/at-spi2/at-spi2-core.conf

- name: Install Python Dependencies
run: |
python -m pip install --upgrade pip
pip install pytest aiohttp websockets opencv-python Pillow numpy qrcode pyzbar

- name: Run E2E Dogtail UI Test (XVFB)
run: |
# Inicia Xvfb com a11y (AT-SPI) dbus-run-session
export DISPLAY=:99
Xvfb $DISPLAY -screen 0 1920x1080x24 &
sleep 3

# Inicia o barramento DBUS e Roda os testes
dbus-run-session -- bash -c "python3 tests/test_ui_dogtail.py"

- name: Run Chaos Monkey Test
run: |
# O teste de chaos precisa do módulo v4l2loopback, que o kernel host do github actions geralmente não permite carregar nativamente sem config avançada.
# Mas validamos a importação sintática do runner de testes:
python3 -m py_compile tests/chaos_hotplug.py
55 changes: 38 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<img src="usr/share/biglinux/bigcam/icons/bigcam.svg" alt="BigCam" width="128" height="128">
</p>

<h1 align="center">BigCam 4.4.4</h1>
<h1 align="center">BigCam 4.5.0</h1>

<p align="center">
<b>The universal webcam control center for Linux — use any camera, including your smartphone, as a professional webcam. No expensive apps needed.</b>
Expand All @@ -21,7 +21,7 @@
</p>

<p align="center">
<img src="https://img.shields.io/badge/Version-4.4.4-brightgreen.svg" alt="Version 4.4.4">
<img src="https://img.shields.io/badge/Version-4.5.0-brightgreen.svg" alt="Version 4.5.0">
<img src="https://img.shields.io/badge/License-GPLv3-blue.svg" alt="License: GPL v3">
<img src="https://img.shields.io/badge/Platform-Linux-green.svg" alt="Platform: Linux">
<img src="https://img.shields.io/badge/GTK-4.0-blue.svg" alt="GTK 4.0">
Expand Down Expand Up @@ -72,7 +72,20 @@
- **Smile Capture removed**: Removed mediapipe-dependent smile detection feature entirely (code, README, translations).
- **i18n verified**: All UI strings confirmed English and translation-ready across 29 languages.

**Version 4.4.4** (current) is the **virtual camera & UX refinement update**:
**Version 4.5.0** (current) is the **detection speed & streaming protocol update**:

- **Parallel camera detection**: All backends (V4L2, GPhoto2, Libcamera, PipeWire) now detect cameras in parallel using `ThreadPoolExecutor`, with incremental result emission — cameras appear as they are found instead of waiting for all backends.
- **Backend priority**: Duplicate cameras are resolved by backend priority (V4L2 > GPhoto2 > Libcamera > PipeWire), keeping the best backend when the same device is detected by multiple backends.
- **USB camera filtering**: Libcamera and PipeWire backends now filter for USB/UVC cameras only, avoiding false positives.
- **Deferred virtual camera**: Virtual camera device creation is deferred until the first video frame renders, reducing startup latency.
- **QUIC/WebTransport** (optional): Phone camera streaming can now use HTTP/3 (QUIC/UDP) via WebTransport when `python-aioquic` is installed. Each video frame travels as an independent QUIC stream (no head-of-line blocking), and audio uses unreliable QUIC datagrams for minimum latency. Falls back to WebSocket (TCP) when unavailable.
- **Adaptive Wi-Fi streaming**: Phone camera auto-adjusts JPEG quality based on WebSocket buffer pressure — reduces quality when congested, restores when clear. Frames are dropped entirely when the buffer exceeds 128 KB.
- **H264/H265/VP9 encoder alignment**: Recording encoders now match big-video-converter defaults — NVENC → VA-API (new) → VA-API (legacy) → Software priority, CQP/CRF rate control, profile high.
- **Incompatible camera detection**: Sony DSLR-A300/A37 and other PTP-only cameras are detected and shown with a warning icon instead of failing silently.
- **Close dialog improvements**: The close confirmation dialog now lists ALL active sources (playing camera, background virtual cameras, phone server, scrcpy, AirPlay) with bullet points.
- **Duplicate toast fix**: Camera notifications are deduplicated by name, preventing repeated toasts when the same physical device is detected by multiple backends.

**Version 4.4.4** is the **virtual camera & UX refinement update**:

- **Label-aware virtual camera allocation**: `allocate_device()` now verifies device labels match the current name template before reusing a v4l2loopback device. Static devices with mismatched labels are skipped — dynamic devices with the correct name are created instead.
- **Stale device cleanup**: `cleanup_dynamic_devices()` now also finds and removes orphaned v4l2loopback devices from previous sessions that weren't tracked. Runs at app startup and during name changes.
Expand All @@ -97,27 +110,35 @@ We are grateful to Rafael and Barnabé for starting this journey.

---

## What's New in 4.4.4
## What's New in 4.5.0

### Virtual Camera
### Detection Speed

- **Label-aware allocation**: Devices are only reused when their card label matches the current name template. Mismatched static devices (from modprobe) are skipped — new dynamic devices are created with the correct name.
- **Stale device cleanup**: Orphaned v4l2loopback devices from previous sessions are automatically cleaned up at startup and during name changes. No more device accumulation across restarts.
- **Duplicate name prevention**: Virtual camera numbering syncs with existing device labels before creating new devices, preventing duplicate "BigCam Virtual 1" names.
- **Background vcam lifecycle**: Toggling or renaming virtual cameras now stops all background pipelines, cleans up devices, and recreates everything with the correct configuration.
- **Parallel backend scanning**: Camera detection is now parallelized across all backends (`ThreadPoolExecutor`), with cameras emitted incrementally as they're found. Typical startup goes from ~4s to ~1s.
- **Backend priority dedup**: When the same camera is detected by multiple backends, the highest-priority one wins (V4L2 > GPhoto2 > Libcamera > PipeWire).
- **USB camera filtering**: Libcamera and PipeWire backends filter for USB/UVC devices, eliminating false positives from virtual or non-camera devices.
- **Deferred virtual camera**: The v4l2loopback virtual camera device is only created after the first video frame renders, cutting perceived startup time.

### Settings
### Phone Wi-Fi Streaming

- **QUIC/WebTransport** (optional): When `python-aioquic` is installed, the phone browser streams via HTTP/3 (QUIC/UDP). Each video frame is an independent QUIC unidirectional stream — no head-of-line blocking between frames. Audio uses QUIC datagrams for minimal latency. Auto-falls back to WebSocket when unavailable.
- **Adaptive quality**: JPEG quality auto-adjusts based on WebSocket buffer pressure (reduces when congested, restores when clear). Frames are dropped entirely when buffer exceeds 128 KB.
- **Protocol indicator**: Stats display now shows "QUIC" or "WS" to indicate active transport.

### Recording

- **Encoder alignment**: H264/H265/VP9 encoders now match big-video-converter defaults — NVENC → VA-API (new) → VA-API (legacy) → Software, CQP/CRF rate control, profile high, proper presets.

### Camera Compatibility

- **Device name apply button**: Changes to the virtual camera name only apply when pressing Enter or clicking the ✓ button — no more device recreation on every keystroke.
- **Keyboard shortcut safety**: Removed `Space` as a capture shortcut (Ctrl+P remains). Single-key shortcuts (Tab, 1/2/3) are suppressed when editing text.
- **Incompatible camera warning**: Sony DSLR-A300/A37 and other PTP-only cameras are detected and shown with a yellow exclamation icon and explanatory message instead of failing silently.
- **Duplicate toast prevention**: Camera discovery notifications are deduplicated by device name, preventing repeated toasts from multi-backend detection.

### Welcome Dialog
### Close Dialog

- **8th feature item**: Added "Advanced Controls" (fine-tune exposure, white balance, per-camera profiles).
- **Grid alignment**: Features use `Gtk.Grid` for consistent row alignment across columns.
- **Window dragging**: Dialog wrapped in `Gtk.WindowHandle` — drag from any empty area.
- **All active sources listed**: The close confirmation now shows ALL active sources (playing camera, background vcams, phone server, scrcpy, AirPlay) with bullet-point names.

### Previous (4.4.1)
### Previous (4.4.4)

### Phone Camera Notifications

Expand Down
2 changes: 1 addition & 1 deletion default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ let
in
stdenv.mkDerivation {
pname = "bigcam";
version = "4.4.4";
version = "4.5.0";

src = ./.;

Expand Down
15 changes: 8 additions & 7 deletions etc/modprobe.d/v4l2loopback.conf
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
# v4l2loopback configuration for BigCam
# exclusive_caps=1,1,1,1 allows WebRTC apps (Chrome, Google Meet, Zoom) to detect ALL cameras
# Fallback config when v4l2loopback-ctl is unavailable.
# When v4l2loopback-ctl IS available, BigCam loads with devices=0
# and creates devices dynamically via v4l2loopback-ctl add.
#
# Parameters:
# devices=4 - Create 4 virtual video devices (one per camera)
# exclusive_caps=1,1,1,1 - Allow browsers/WebRTC apps to see ALL cameras (per-device)
# max_buffers=4 - Buffer size for smooth streaming
# video_nr=10,11,12,13 - Use /dev/video10-13
# devices=5 - Create 5 virtual video devices (one per camera)
# exclusive_caps=1,... - Allow browsers/WebRTC apps to see ALL cameras
# max_buffers=8 - Larger buffer to absorb USB timing jitter
# video_nr=20-24 - Use /dev/video20-24 (avoids physical camera collisions)
# card_label - Friendly name shown in apps
#
options v4l2loopback devices=4 exclusive_caps=1,1,1,1 max_buffers=4 video_nr=10,11,12,13 card_label="BigCam Virtual 1,BigCam Virtual 2,BigCam Virtual 3,BigCam Virtual 4"
# Or copy to /etc/modprobe.d/ for persistence
options v4l2loopback devices=5 exclusive_caps=1,1,1,1,1 max_buffers=8 video_nr=20,21,22,23,24 card_label="BigCam Virtual 1,BigCam Virtual 2,BigCam Virtual 3,BigCam Virtual 4,BigCam Virtual 5"
99 changes: 99 additions & 0 deletions tests/chaos_hotplug.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
#!/usr/bin/env python3
"""
Chaos Hotplug - Simula conexão e desconexão agressiva de dispositivos V4L2.
Objetivo: Garantir que o EventBus e a UI do BigCam não sofram deadlock.
"""

import os
import sys
import time
import random
import threading
import logging
from typing import List

# Ensure bigcam modules are importable
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../usr/share/biglinux/bigcam")))

from utils.command_runner import SecureCommandRunner

logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
log = logging.getLogger("ChaosMonkey")

class ChaosHotplugger:
def __init__(self, num_devices=3):
self.num_devices = num_devices
self.runner = SecureCommandRunner()
self.active_devices: List[str] = []
self.running = False
self.thread = None

def start(self):
self.running = True
self.thread = threading.Thread(target=self._chaos_loop, daemon=True)
self.thread.start()
log.info("Chaos Monkey iniciado.")

def stop(self):
self.running = False
if self.thread:
self.thread.join()
# Clean up
for dev in list(self.active_devices):
self._remove_device(dev)
log.info("Chaos Monkey finalizado.")

def _add_device(self) -> str:
# We rely on pkexec / sudo rules being set up for v4l2loopback-ctl
dev_num = random.randint(50, 99)
dev_path = f"/dev/video{dev_num}"
if dev_path in self.active_devices:
return ""

log.info(f"Adding chaos device: {dev_path}")
success, _, _ = self.runner.run_sync(
["sudo", "-n", "v4l2loopback-ctl", "add", "-n", f"ChaosCam {dev_num}", dev_path],
timeout=5.0
)
if success:
self.active_devices.append(dev_path)
return dev_path
return ""

def _remove_device(self, dev_path: str):
if dev_path in self.active_devices:
log.info(f"Removing chaos device: {dev_path}")
self.runner.run_sync(
["sudo", "-n", "v4l2loopback-ctl", "delete", dev_path],
timeout=5.0
)
self.active_devices.remove(dev_path)

def _chaos_loop(self):
while self.running:
action = random.choice(["add", "remove", "add", "add"])

if action == "add" and len(self.active_devices) < self.num_devices:
self._add_device()
elif action == "remove" and self.active_devices:
dev_to_remove = random.choice(self.active_devices)
self._remove_device(dev_to_remove)

time.sleep(random.uniform(0.1, 1.5))


if __name__ == "__main__":
if os.geteuid() != 0 and not os.system("sudo -n true") == 0:
log.error("This test requires sudo-nopasswd for v4l2loopback-ctl.")
sys.exit(1)

chaos = ChaosHotplugger(num_devices=5)
chaos.start()

try:
log.info("Running hotplug chaos for 30 seconds...")
time.sleep(30)
except KeyboardInterrupt:
log.info("Interrupted by user")
finally:
chaos.stop()
57 changes: 57 additions & 0 deletions tests/test_event_bus.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""Unit tests for the global EventBus."""

import sys
import os
import pytest

# Add the src path so we can import modules
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../usr/share/biglinux/bigcam')))

from core.event_bus import event_bus

def test_event_bus_singleton():
"""Ensure the EventBus is a singleton."""
from core.event_bus import EventBus
bus1 = EventBus()
bus2 = EventBus()
assert bus1 is bus2
assert bus1 is event_bus

def test_event_bus_emit_camera_changed():
"""Test emitting the camera-changed signal."""
emitted = False
received_cam = None

def on_camera_changed(bus, cam_info):
nonlocal emitted, received_cam
emitted = True
received_cam = cam_info

handler_id = event_bus.connect("camera-changed", on_camera_changed)
event_bus.emit("camera-changed", "fake_camera_info")

assert emitted is True
assert received_cam == "fake_camera_info"

event_bus.disconnect(handler_id)

def test_event_bus_emit_mobile_status():
"""Test emitting the mobile-status-changed signal."""
emitted = False
received_backend = None
received_status = None

def on_mobile_status(bus, backend, status):
nonlocal emitted, received_backend, received_status
emitted = True
received_backend = backend
received_status = status

handler_id = event_bus.connect("mobile-status-changed", on_mobile_status)
event_bus.emit("mobile-status-changed", "phone", "connected")

assert emitted is True
assert received_backend == "phone"
assert received_status == "connected"

event_bus.disconnect(handler_id)
Loading
Loading