Skip to content

Replace rpicam-still/rpicam-vid with picamera2 daemon, add WebSocket …#2813

Open
nevion wants to merge 1 commit into
aaronwmorris:devfrom
nevion:dev
Open

Replace rpicam-still/rpicam-vid with picamera2 daemon, add WebSocket …#2813
nevion wants to merge 1 commit into
aaronwmorris:devfrom
nevion:dev

Conversation

@nevion
Copy link
Copy Markdown

@nevion nevion commented Mar 25, 2026

Replace rpicam-still/rpicam-vid with picamera2 daemon + WebSocket live stream

Problem

The current libcamera backend spawns separate processes (rpicam-still for capture, rpicam-vid for streaming) that fight over exclusive camera access. The live stream must stop/restart the entire indi-allsky service to acquire the camera, and vice versa.

Solution

A single-process picamera2 daemon that owns the camera and serves both capture and streaming simultaneously via IPC (shared memory + Unix socket).

Changes

Camera daemon (picamera2_daemon.py):

  • Standalone process owning the Picamera2 instance
  • Continuous grab loop in daemon thread
  • Shared memory for JPEG stream (dynamically sized from stream resolution)
  • Unix socket IPC for controls, capture, metadata, sensor info
  • Server-side OSD overlay using the original indi-allsky overlay modules (orb, cardinals, moon texture, Pillow text with # color/xy/anchor/size directives)
  • Stream resolution, quality, and OSD state persisted to config DB

Client (picamera2_client.py):

  • Zero-dependency client for capture worker and Flask
  • Shared memory reader with resource tracker fix (prevents shm deletion on client exit)

libcamera.py refactor:

  • setCcdExposure uses daemon client instead of rpicam-still subprocess
  • Async capture via background thread
  • Metadata from daemon instead of JSON file parsing
  • No colorspace conversions (BGR throughout)

WebSocket live stream:

  • Flask WebSocket endpoint reads directly from daemon shared memory
  • Binary frame protocol: [4B JSON len][metadata JSON][JPEG data]
  • Metadata: exposure, gain, lux, colour_temp, sensor_temp, WB gains
  • Sends latest frame immediately on connect (no stall during long exposures)
  • AllskyPlayer JS: canvas-based WS player with MJPEG fallback

Camera controls:

  • Unified sliders (gain, exposure, brightness, contrast, target ADU, JPEG quality)
  • Resolution selector with hardware/software mode labels
  • OSD toggle (persisted)
  • All changes apply live via daemon socket

Infrastructure:

  • picamera2-daemon.service: systemd unit (starts before indi-allsky)
  • wsgi.py: root redirect for direct SSL without reverse proxy
  • _stop_allsky/_start_allsky removed (no camera contention)
  • Runs on HTTPS/443 via gunicorn SSL — no Apache/nginx needed
  • flask-sock added to requirements

Testing

  • Tested on Raspberry Pi 5 with IMX415 sensor
  • Concurrent capture + streaming verified (no contention)
  • Day/night mode transitions, long exposures (25s+), binning changes
  • WebSocket stream delivers first frame in <500ms even during long exposures

@aaronwmorris
Copy link
Copy Markdown
Owner

I really like this possibility, but I cannot integrate this change as-is. The changes to libcamera.py would need to be reverted and changes would need to be integrated as a new camera interface. Something like picamera2_socket.py would be fine.

I am not sure if the other changes are breaking or not.

@nevion
Copy link
Copy Markdown
Author

nevion commented Mar 29, 2026

the problem is just that you want libcamera support as the default right? It is not too much work to use python-libcamera to support the low level library in the daemon rather than calling the standalone binaries. I've only used libcamera for rpis but apparently it covered a few more cases.

@aaronwmorris
Copy link
Copy Markdown
Owner

It is not about it being the default. The current libcamera functionality is currently used by hundreds of people and I need to keep supporting the current method of functionality.

Adding a new camera interface is preferred since it allows people to keep using what works, but the additional interface can be used by different users.

@nevion nevion force-pushed the dev branch 2 times, most recently from bc5da19 to 26c93a5 Compare March 30, 2026 06:45
…live stream

Single-process picamera2 daemon owns the camera and serves frames via shared
memory + Unix socket IPC. Three camera backends: picamera2 (preferred),
python3-libcamera (raw bindings), libcamera-still (subprocess fallback).

Camera backends (camera_backend.py):
- Picamera2Backend, LibcameraBackend, LibcameraStillBackend with auto-detection
- LibcameraBackend: control lookup via getattr(libcamera.controls, name)
- LibcameraStillBackend: capture lock, timeout wrapper, stale process cleanup,
  sticky controls, fixed temp filenames, --awb off support
- requested_exposure/gain injected into metadata from pending controls

Daemon (picamera2_daemon.py):
- Backend-agnostic grab loop, set_backend hot-switch via IPC
- Watchdog thread: auto-restarts backend if grab loop stalls for 120s
- _last_applied tracks controls across frames for OSD synchronization
- _capture_time stamped for frame staleness detection

Stream and display:
- WebSocket preferred (works with gthread via flask-sock/simple-websocket)
- /api/stream/frame.jpg: single-frame polling with requestAnimationFrame pacing
- /api/stream/metadata: reads directly from daemon socket (no stale cache)
- allsky_player.js: _pollInFlight guard, frame_age warning display
- OSD shows requested exposure/gain rounded to 2 decimals

Dashboard camera controls (capture_buttons.js):
- Driver selector (picamera2/libcamera/libcamera-still)
- Auto-exposure toggle (TARGET_ADU_DISABLE) with config reload trigger
- Exposure slider loads from config DB (CCD_EXPOSURE_MAX), not live metadata
- Gain syncs from live metadata, exposure stays at user-set value
- Config saves via UPDATE (upsert), not INSERT, to prevent row proliferation

Config and capture:
- LIBCAMERA.BACKEND field, TARGET_ADU_DISABLE flag
- image.py: skip calculate_exposure when TARGET_ADU_DISABLE is set
- Slider updates CCD_EXPOSURE_MAX + all mode gains simultaneously
- Config level preserved on save, capture worker reload queued on changes
- PYTHONDONTWRITEBYTECODE=1 in systemd services
@nevion
Copy link
Copy Markdown
Author

nevion commented Apr 11, 2026

@aaronwmorris there you go, leaves the subprocess version and allows switching drivers + disabling auto exposure, these settings are synchronized to the backend configuration.
It still adds/has the far more efficient and faster libcamera python bindings and the rpi optimized camera2 library
image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants