Architecture#

Overview#

MOSAIC is built on Qt 6 / C++23 and organised into five layers:

UIQt Widgets + QML
VideoSettingsW · AudioSettingsW · TriggerSettingsW · CalibrationW · LoggerPanelW · LoginDialog · MonitorView.qml
Managersmain-thread coordinators
VideoManager · AudioManager · TriggerManager · RecordManager
Workersbackground threads
VideoGrabber · VideoEncoder · ParallelPortTrigger · (audio: QAudioSource)
Coresettings, auth, logging
Application · AppSettings · ProfileManager · Logger
Utilsheader-only, no Qt dependency
RingBuffer<T> · elapsed_ns() · wall_clock_ns()

Startup sequence#

1
ProfileManager::load()
Reads the auth manifest of known profiles.
2
LoginDialog::exec()
Blocks until a profile is chosen.
3
Application::initialize(username)
Logger::open_log_file()AppSettings::load() (reads ~/.config/CSRU/mosaic/profiles/<user>/settings.json) → TriggerManager() (installs keyboard event filters immediately) → AudioManager()VideoManager::open() (opens cameras, or stubs) → RecordManager()
4
MainWindow::show()
The app is now interactive.

Video pipeline#

Each configured camera runs its own pair of threads:

Camera (Pylon)
or a test-pattern stub if none is connected
VideoGrabber
QThread, pushes VideoFrame
RingBuffer<VideoFrame>
lock-free SPSC, capacity 128
VideoEncoder
QThread
video_N.mp4
FFmpeg / NVENC
timestamps_camN.csv
FrameTimestampWriter

VideoManager owns all (Grabber, Buffer, Encoder) triples and starts / stops them together via RecordManager.

Thread safety#

  • RingBuffer<T> is a lock-free SPSC structure (one producer, one consumer). Each camera has its own ring so there is no cross-camera contention.

  • FrameTimestampWriter::write() is mutex-protected (called from grabber thread, stop() from main thread).

  • Logger::log() is mutex-protected; the entry_added signal is emitted from the calling thread — connect with Qt::QueuedConnection when the receiver lives on the main thread.

Session output#

A recording session creates the following files:

recordings/ └── 2026-06-04_14-32-05/ ├── session_meta.json # cameras, mics, triggers, recorded_by, start UTC ├── trigger.csv # elapsed_ms, wall_clock, source, label, value ├── sync_manifest.json # written after recording stops ├── audio/ │ └── audio_0.wav # one per microphone └── video/ ├── video_0.mp4 # one per camera └── timestamps_cam0.csv # frame_id, elapsed_ns, wall_ns, hw_timestamp_ns (per camera)

Settings persistence#

Settings are stored as JSON (schema v1) in a per-profile directory:

~/.config/CSRU/mosaic/ profiles.json # auth manifest (hashed passwords) profiles/ <username>/ settings.json # full AppSettings (cameras, audio, triggers, record) mosaic.log

Switching profiles (File Switch profile) exits with code 42; main() detects this and re-shows the login dialog, rebuilding the entire Application + MainWindow with the new user’s settings.

Feature-flag guards#

Hardware subsystems are compiled under preprocessor guards so the software runs on any developer machine without lab hardware:

Guard

What it enables

MOSAIC_HAVE_CAMERAS

Basler Pylon camera access in VideoGrabber

MOSAIC_HAVE_FFMPEG

FFmpeg encode path in VideoEncoder

MOSAIC_HAVE_NVENC

NVIDIA NVENC codec selection

MOSAIC_HAVE_VIDEOTOOLBOX

Apple VideoToolbox codec selection

MOSAIC_HAVE_OPENCV

Calibration in CalibrationManager

MOSAIC_HAVE_PARALLEL_PORT

InpOut32 polling in ParallelPortTrigger